Аудитор: серверний Claude (Opus 4.7) Метод: read-only статичний аналіз
/srv/projects/lunar-hubble/(live) +/srv/projects/family-tree/(duplicate) Без модифікацій коду, без запитів до продакшн URL на запис, безnpm install
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, Gemini API key AIzaSyBqjufdGYbnhl4sC9EufMF3UprSNkQBrNM, Telegram bot token 8342933869:AAGsxuJpfd8yuwyIsC1LWoKKPOxy-bYCEr0, приватний RSA ключ Google Service Account (tree_bot/credentials.json). Всі ці секрети треба ротувати сьогодні ж.USING (true) для ALL, тобто будь-хто з публічним anon-ключем може читати/писати/видаляти всі записи. Таблиця persons/relationships взагалі не згадана в міграціях — невідомо, чи ввімкнено RLS, але policies для них відсутні.1234/0000 хардкоджено в клієнтському JS (src/App.tsx:280-283), підказка з цими PIN друкується на формі логіну (src/App.tsx:316). Плюс invite-token family-tree-invite-24 — статичний рядок у клієнті. Будь-який гість дивиться DevTools і стає адміном. Фактично — публічний сайт без доступу.check-error*.js, guess-cols.js), screenshots, console dumps, дубльовані імпортери (import-data.js + importData.js), artifact vite.config.ts.timestamp-*.mjs, .pyc кеш. Це не просто бруд — це прикривало факт того, що секрети витекли.tree_bot/backup.py (не в cron), нема CI/CD, нема моніторингу, TypeScript strict-mode визначений але в коді as any щедро, є React помилка в console_dump_2 "child has more than 1 parent" (граф ламається на некоректних даних).Загальна готовність до production: ~25%. Візуально працює, але безпека = 0. Для батька-некодера це ок як MVP-чорновик у приватному середовищі, але НЕ як публічний Cloud Run сервіс.
/srv/projects/lunar-hubble/.git/config + /srv/projects/family-tree/.git/config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 — Serhii's PAT, дає push/pull на обидва репозиторії під serhiivereschak.# 1. Revoke token: https://github.com/settings/tokens
# 2. Видалити токен з remote URL:
cd /srv/projects/lunar-hubble
git remote set-url origin https://github.com/serhiivereschak/lunar-hubble.git
cd /srv/projects/family-tree && git remote set-url origin https://github.com/serhiivereschak/family-tree.git
# 3. Створити новий PAT, зберегти у /srv/passepartout/github/tokens.txt
# 4. Для auth — використати git credential helper або SSH ключ
tree_bot/.env (закомічено в git)/srv/projects/lunar-hubble/tree_bot/.env:2AIzaSyBqjufdGYbnhl4sC9EufMF3UprSNkQBrNM/srv/passepartout/google/gemini.key, передавати в runtime через Cloud Run env vars (не в образ).tree_bot/.env (закомічено в git)/srv/projects/lunar-hubble/tree_bot/.env:18342933869:AAGsxuJpfd8yuwyIsC1LWoKKPOxy-bYCEr0/revoke у @BotFather (або /token для нового), оновити /srv/passepartout/telegram/tree-bot.token, підтягувати через env в Cloud Run./srv/projects/lunar-hubble/tree_bot/credentials.json (рядки 1-13, включно з -----BEGIN PRIVATE KEY-----)tree-bot@gen-lang-client-0741142304.iam.gserviceaccount.comGDRIVE_FOLDER_ID=1uLZRMypfAgpmAfh3nlE1j40eHb0GkbQp.1. GCP Console → IAM → Service Accounts → tree-bot → Keys → Delete key e14e8637dd6caa08e226913351df1de861e460ad
2. Створити новий ключ, зберегти ПОЗА репо в /srv/passepartout/google/tree-bot-sa.json
3. В Cloud Run монтувати як secret через Secret Manager
4. Додати credentials.json у .gitignore
.env:1-2, tree_bot/.env:5-6, src/lib/supabase.ts (через import.meta.env), а також хардкодом: check-error.js:3, check-error2.js:3, check-persons.js:3, check-schema.js:5, get-schema.js:3, guess-cols.js:3, import-data.js:5, importData.js:5, remove-orphans.js:3.sb_publishable_7-WKYOWu54hJ9wYPQY6vfQ_XnFb5vebhttps://kgjhghdopikkjdotacof.supabase.cosrc/lib/supabase.ts з import.meta.env..env видалити з git (git rm --cached .env && echo ".env" >> .gitignore)./srv/projects/lunar-hubble/supabase_migrations.sql:48-82CREATE POLICY "Allow public all-access" ON public.stories FOR ALL USING (true);
-- те саме для: story_participants, sources, source_citations, photos, photo_participants
persons та relationships навіть не згадані в міграціях. Згідно db.py/фронтенду — вони існують і доступні через публічний endpoint. Невідомо, чи ввімкнено для них RLS взагалі. Якщо ні — anon key = full DELETE/UPDATE доступ.-- В Supabase SQL editor:
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname='public';
SELECT * FROM pg_policies WHERE schemaname='public';
-- Читати можуть усі (публічне родовідне)
CREATE POLICY "read_all" ON persons FOR SELECT USING (true);
-- Писати можуть лише автентифіковані
CREATE POLICY "admin_write" ON persons FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "admin_update" ON persons FOR UPDATE TO authenticated USING (true);
CREATE POLICY "admin_delete" ON persons FOR DELETE TO authenticated USING (true);
anon-ключ тільки для SELECT, а для CRUD — service_role у Telegram-боті (серверний бік) або Supabase magic-link./srv/projects/lunar-hubble/src/App.tsx:280 (if (pinInput === '1234')), :283 ('0000'), :316 (текст «Підказка: Адмін = 1234, Родич = 0000»).view-source = повний адмін-доступ. Навіть без підказки на сторінці — клієнтський JS після vite build мінимізований, але PIN залишаються plain strings.family-tree-invite-24 (App.tsx:272) теж хардкод, гість отримує читання з ?token=... — але поточні RLS полиси дають write-усьому незалежно від "ролі" у клієнті.invites, перевіряти на сервері)./srv/projects/family-tree/ (push на github.com/serhiivereschak/family-tree.git)/srv/projects/family-tree/ локально, архівувати GitHub-репо (або зробити приватним і видалити). Per wiki — репо визначено як duplicate для видалення.check-error.js, check-error2.js, check-error3.js (не читав, але розмір 329-456B — такі самі), check-error4.js, check-persons.js, check-schema.js, get-schema.js, guess-cols.jsconsole_dump.txt, console_dump_2.txt, boxes_dump.json (=[]), f3data_dump.json, graph_dump.html (97KB!), svg_dump.htmldump_debug.cjs, dump_svg.cjs, screenshot.cjs, my_screenshot.png (117KB)vite.config.ts.timestamp-1775571671298-03edb53fc6823.mjsquery.py (порожній)tree_bot/__pycache__/db.cpython-312.pyc (бінарка в git)tree_bot/bot_log.txt (PowerShell traceback, Windows-шлях D:\Users\Sergey.Vereschak.DELTAMEDICAL\... — витік хоста розробника)git rm усі перелічені, додати в .gitignore (*.pyc, __pycache__/, *_dump*, *.timestamp-*.mjs, screenshot*.png, check-*.js, dump_*)./srv/projects/lunar-hubble/import-data.js (читає ../import.json)/srv/projects/lunar-hubble/importData.js (читає ../inferred_tree.json).delete().neq('id', '00000000...')) — небезпечно без бекапу, доступні через публічний anon-ключ.scripts/migrate.js, додати dry-run мод, документувати./srv/projects/family-tree/parse-svg-graph.js, parse-geom.js — legacy парсер з SVG Famsoft/whatever → JSON. Не використовується в lunar-hubble.derevo_rodu (1).html — 36KB standalone UMD-prototype з CDN-React (unpkg), tailwind.config inline. Це попередник lunar-hubble.rm -rf /srv/projects/family-tree/ після переконання, що все потрібне перенесено. Архівувати GH-репо.tsconfig.json:18-21 — strict, noUnusedLocals, noUnusedParameters.src/App.tsx:115,161 (as any), src/components/D3Graph.tsx:86,237,268 (any[], any в ext), src/components/RelationalManagers.tsx:6 (person: any), src/components/NodeDrawer.tsx:33 (extendedData: any).npm run lint у CI; додати тип ExtendedPersonData та прибрати any.D3Graph.tsx:170-203 не перевіряє, що edge у dagre формує дерево. При не-деревоподібних даних dagre кидає exception → React вішає UI.dagre.layout(dagreGraph) у try/catch; або використати alternate graphlib layout для DAG; додати ErrorBoundary на <D3Graph>.App.tsx:107 — if (p.notes && p.notes.startsWith('{')) { ext = JSON.parse(p.notes); Object.assign(fullPerson, ext); fullPerson.notes = ext.notes || ''; }notes у БД — це серіалізований JSON усіх extended fields (birth_date, birth_place, email, phone, x_pos, y_pos, documents, photos тощо).persons не мала цих колонок, потім add_columns.sql (у family-tree!) додав їх, але дані досі в notes як JSON.person.birth_date vs JSON.parse(notes).birth_date); GlobalMediaModal пише документи/фото назад у notes + флаттить всі колонки.notes → розкласти по колонках → очистити notes → в коді прибрати JSON.parse(notes) логіку.supabase_migrations.sql містить тільки stories/sources/photos/participants — НЕМАЄ create-ів для persons, relationships.family-tree/add_columns.sql, add_contacts.sql, add_x_pos.sql, повне_оновлення_бази.sql — окремі файли в старому репо.supabase/migrations/0001_init.sql, 0002_extended_columns.sql і т.д. або зробити supabase init у проекті.tree_bot/backup.py є — вивантажує всі таблиці в JSON і пише на Google Drive.python3 backup.py). Нема cron. Нема alerting при failure./srv/... з копією в /srv/backups/lunar-hubble/.import-data.js / importData.js TRUNCATE без confirmation--confirm flag, dry-run default, вивід diff.bot.py:95-167 — обробляє текст/фото/voice/doc без квоти.ALLOWED_USERS=312194208 — тільки 1 користувач (батько?), але якщо інші пробі — 1 reply-повідомлення, нема penalty/jail.file_id фото-завантаження → Google Drive квота + Gemini 2.5 Pro квота $ (дорого).asyncio.wait_for(send_message, timeout=30)..github/workflows/, cloudbuild.yaml).gcloud run deploy з локального хоста..github/workflows/deploy.yml з gcloud auth, gcloud run deploy --source, трігер на push до main.bot.py лише print().console.error + alert('Помилка: ' + error.message).aria-label на всіх кнопках.<title>Family Tree</title> (не укр.), немає Open Graph для шарингу запрошень у WhatsApp/Viber.gdrive.py — зараз GPS-метадата фото завантажується "як є".src/lib/gedcom.ts:3 exportToGedcom). У wiki сказано "GEDCOM імпорт/експорт". Імпорту НЕМАЄ — wiki збрехала.relationship_type = string вільного формату ('couple', 'parent_child'). Enum + CHECK constraint у БД. Немає підтримки adopted/step-parent/divorced.first_name/last_name/maiden_name підрядок. Не шукає по дата-народження, місцю, професії.supabase.from('persons').select('*') без limit — коли дерево виросте до 1000+ людей, буде ~500KB JSON на load. Pagination/streaming.package.json є react-force-graph-3d — не використовується. Або викинути, або ввімкнути fancy 3D-view.tau-prolog) — напевно для inference родинних зв'язків (онук = child of child). Не використовується в коді. Видалити.| Вектор | Стан | Рейтинг |
|---|---|---|
| Authentication | PIN hardcoded, invite-token static | 🔴 0/10 |
| Secrets in git | 4 класи секретів у репо, всі push'нуті на GitHub | 🔴 0/10 |
| Supabase RLS | ALL USING (true) — RLS ввімкнено, але дозволяє все |
🔴 1/10 |
| XSS | React JSX за замовчуванням екранує. split(/https?:/) у RelationalManagers.tsx:143 рендерить частини в <a href> — якщо url містить javascript:, вставиться. |
🟡 6/10 |
| CSRF | Stateless, Supabase auth через Bearer header — CSRF не релевантний. | 🟢 9/10 |
| CORS | Supabase дефолт (*) + Cloud Run без обмежень — публічна API. |
🟡 4/10 |
| Cloud Run public | https://lunar-hubble-105435845108.europe-west1.run.app/ — без auth gate, intended. Сам по собі OK, але пара з PIN-theater = проблема. |
🟡 5/10 |
| Telegram bot auth | ALLOWED_USERS=312194208 (white list) — OK. Але бот має доступ до Supabase anon key (а не service_role) — дивно. |
🟡 6/10 |
Додаткові деталі:
src/components/RelationalManagers.tsx:143-147 — наївний regex-linkify. javascript:alert(1) як URL у історії → клік = XSS. Дрібно, але релевантно.tree_bot/db.py:10-11 — приймає і VITE_SUPABASE_KEY і SUPABASE_KEY. Anon key дає боту admin-powers через слабкі RLS. Має використовуватися SUPABASE_SERVICE_ROLE_KEY (ніколи не віддавати клієнту).gdrive.py:39-42 — 'type': 'anyone', 'role': 'reader' — всі фото з Google Drive є публічно доступними за прямим лінком. Приватні сімейні фото = публічний інтернет. Потрібно як мінімум authenticated-access через Supabase Storage замість.any в проді коді..eslintrc.cjs), npm run lint є, але судячи по артефактах — не запускається.try { } catch { } без logging місцями (App.tsx:112, D3Graph.tsx:48, :119). Silent failure = складно діагностувати.ext.[type]: JSON.stringify(items) у GlobalMediaModal.tsx:109 + person.notes = JSON.stringify(ext) — JSON-in-JSON double-nested. Технічний борг зростатиме.npm audit (read-only), але версії в порядку станом на 2026-04.persons + relationships (з relationship_type string) — класичний, но relationship без enum/дати-від/дати-до (не можна моделювати розлучення, усиновлення).notes як dump JSON) — технічний борг, який тягнеться з першої ітерації./family-tree/ як add_*.sql.search_persons/add_story/add_source/add_photo як tools. Недолік — бот пише в БД через Supabase REST API з anon key (має бути service_role).Dockerfile багатостадійний (builder + nginx:alpine) — OK. nginx.conf базовий, без gzip, без security headers (Content-Security-Policy, X-Frame-Options, Referrer-Policy).remove-orphans.js existed — тобто orphans з'являлися. Ймовірна причина: delete relationship залишає людину без зв'язків (OK concept), але insert у handleCreateNew (App.tsx:147-154) створює "Нова Особа" без relationship.tree_bot/backup.py манускрипт, не в cron.story_participants.person_id REFERENCES persons(id) ON DELETE CASCADE — добре, видалення людини не залишить сиріт у participants.relationships — FK constraints не видно (бо цих CREATE TABLE у supabase_migrations.sql нема)..env файли в git (катастрофа); немає Google Secret Manager.vite.config.ts.timestamp-1775571671298-03edb53fc6823.mjs — артефакт Vite, який потрапив у image bake. Не критично, але брудно.Плюси:
min-h-[48px] і min-h-[60px] — дотримано WCAG target-size.Мінуси:
text-lg для meta-інфо = 18px, це мало для 60+. text-xl (20px) краще.aria-label не знайдено при grep.App.tsx:219), гість бачить пустий canvas — loading spinner без контексту./revoke у @BotFather + new у /srv/passepartout/telegram/tree-bot.token.env / credentials.json з git history (20 хв):cd /srv/projects/lunar-hubble
git rm --cached .env tree_bot/.env tree_bot/credentials.json tree_bot/__pycache__/db.cpython-312.pyc
echo -e "\n.env\ntree_bot/.env\ntree_bot/credentials.json\n__pycache__/\n*.pyc" >> .gitignore
git commit -m "chore: remove secrets from tracking"
# Для history-rewrite — git filter-repo або BFG Repo-Cleaner (не обов'язково, якщо репо приватне і токени ротовані)
git push
git remote set-url origin git@github.com:serhiivereschak/lunar-hubble.git
# або https://github.com/... з credential-helper
src/App.tsx:315-317 — видалити параграф з підказкою. Залишити тільки invite-link (але це теж треба фіксити).-- Для кожної таблиці: drop public all-access, replace with:
DROP POLICY "Allow public all-access" ON persons;
CREATE POLICY "read_public" ON persons FOR SELECT USING (true);
CREATE POLICY "write_authenticated" ON persons FOR INSERT TO authenticated WITH CHECK (true);
-- ... UPDATE, DELETE
/srv/projects/family-tree/ + архівувати GH repo.--dry-run і --confirm.supabase/migrations/ versioned, одна команда apply.notes JSON → окремі колонки (one-shot SQL script).| Область | Рейтинг | Коментар |
|---|---|---|
| Security | 2/10 | 4 класи витоку секретів у публічний GitHub-репо, RLS анонімно-адмін. Все інше неважливо на тлі цього. |
| Code quality | 4/10 | Основний код непоганий, але 1/3 репо — сміття. TS strict decorative. |
| Architecture | 5/10 | Вибір стеку ок, модель даних ок для MVP, міграції disaster, JSON-in-notes — борг. |
| UX (для 60+) | 6/10 | Великі кнопки, українська, Telegram-бот — розумно. PIN форма і мінорні mobile issues. |
| Deployment | 3/10 | Працює, але ручно, без CI, без моніторингу, секрети в образі. |
| Data integrity | 4/10 | Backup existed but manual, orphans проблема відома, немає FK для relationships. |
Проєкт працює — це дуже важливо. Батько може користуватися. Але:
Рекомендація: тримати Cloud Run URL приватним (Cloud Run IAM → лише автентифіковані GCP користувачі, або IAP-gate) до моменту, поки не виконано кроки 1-6 з плану. Або — що простіше — приховати посилання, не ділитися з родичами до виконання кроків 1-4.