From f0d39af6dc0b34535254e9f683630e10a75d9de1 Mon Sep 17 00:00:00 2001 From: Sergei Date: Thu, 22 Jan 2026 18:52:01 -0800 Subject: [PATCH] Add security audit report and PRD for custom names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT_REPORT.md: - Full security audit (90 findings reviewed) - 6 critical tasks for immediate fix - 45 recommendations for later - Complete RLS implementation plan (1-2 weeks) - Doppler for secrets management - Winston + Sentry for logging PRD.md: - Personalized beneficiary names feature - custom_name in user_access table - Backend + Frontend tasks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- AUDIT_REPORT.md | 1250 +++++++++++++++++++++++++++++++++++++++++++++++ PRD.md | 150 ++++++ 2 files changed, 1400 insertions(+) create mode 100644 AUDIT_REPORT.md create mode 100644 PRD.md diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md new file mode 100644 index 0000000..7c414a6 --- /dev/null +++ b/AUDIT_REPORT.md @@ -0,0 +1,1250 @@ +# WellNuo Security & Quality Audit Report + +**Дата:** 2026-01-22 +**Версия:** 2.0 (финальная после ревью) + +--- + +## Резюме + +После ревью из 90 найденных пунктов: +- **6 задач** — делаем обязательно (критичные) +- **45 рекомендаций** — делаем потом +- **39 пунктов** — убрали (не актуально, дубликаты, false positive) + +--- + +# ЧАСТЬ 1: Обязательные задачи + +## Критичные задачи (делаем сейчас) + +### 1. VULN-001: Stripe Webhook Signature Required + +**Файл:** `backend/src/routes/webhook.js:22-28` + +**Проблема:** Webhook signature verification можно обойти если `STRIPE_WEBHOOK_SECRET` не установлен: +```javascript +if (webhookSecret) { + event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret); +} else { + event = JSON.parse(req.body.toString()); // Нет проверки! +} +``` + +**Риск:** Атакующий может подделать Stripe webhooks → создать заказы без оплаты. + +**Fix:** Сделать `STRIPE_WEBHOOK_SECRET` обязательным — проверка на старте сервера: +```javascript +if (!process.env.STRIPE_WEBHOOK_SECRET) { + console.error('STRIPE_WEBHOOK_SECRET is required!'); + process.exit(1); +} +``` + +**Effort:** 1 час + +--- + +### 2. VULN-003: JWT Secret Validation on Startup + +**Файл:** `backend/src/index.js` + +**Проблема:** API запускается без проверки силы JWT_SECRET. + +**Риск:** Слабый secret = брутфорс токенов. + +**Fix:** Добавить проверку на старте: +```javascript +if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { + console.error('JWT_SECRET must be at least 32 characters!'); + process.exit(1); +} +``` + +**Effort:** 30 минут + +--- + +### 3. VULN-004: OTP Rate Limiting + +**Файл:** `backend/src/routes/auth.js:92` + +**Проблема:** 6 цифр = 900,000 вариантов, нет rate limiting на verify-otp. + +**Риск:** Брутфорс OTP за минуты — атакующий может войти в любой аккаунт. + +**Fix:** Добавить rate limit: +```javascript +const rateLimit = require('express-rate-limit'); + +const otpLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 минут + max: 5, // максимум 5 попыток + message: { error: 'Too many attempts, try again in 15 minutes' }, + keyGenerator: (req) => req.body.email || req.ip +}); + +router.post('/verify-otp', otpLimiter, async (req, res) => { ... }); +``` + +**Effort:** 2 часа + +--- + +### 4. VULN-005: Input Validation + +**Файлы:** `beneficiaries.js:356`, `stripe.js:54`, `invitations.js:238` + +**Проблема:** Пользовательский ввод не валидируется. + +**Риск:** SQL injection, XSS, некорректные данные в БД. + +**Fix:** Добавить express-validator: +```bash +npm install express-validator +``` + +```javascript +const { body, validationResult } = require('express-validator'); + +router.post('/beneficiaries', + body('name').isString().trim().isLength({ min: 1, max: 200 }), + body('email').optional().isEmail(), + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + // ... остальной код + } +); +``` + +**Effort:** 4 часа + +--- + +### 5. VULN-007: Secrets Management (Doppler) + +**Проблема:** Все секреты хранятся в `.env` файле на сервере. + +**Текущие секреты:** +- DB_PASSWORD +- JWT_SECRET +- BREVO_API_KEY +- STRIPE_SECRET_KEY +- STRIPE_WEBHOOK_SECRET +- ADMIN_API_KEY +- LEGACY_API_PASSWORD +- LIVEKIT_API_KEY +- LIVEKIT_API_SECRET + +**Fix:** Перейти на Doppler: + +1. Регистрация на doppler.com +2. Создать проект WellNuo +3. Добавить все секреты через UI +4. Установить CLI на сервер: + ```bash + curl -Ls https://cli.doppler.com/install.sh | sh + doppler login + doppler setup + ``` +5. Изменить запуск в PM2: + ```bash + doppler run -- node index.js + ``` +6. Удалить `.env` файл + +**Effort:** 2-3 часа + +--- + +### 6. VULN-008: Dependency Vulnerability Fix + +**Проблема:** Пакет `qs` имеет известную DoS уязвимость. + +**Fix:** +```bash +cd backend +npm update qs +npm audit fix +``` + +**Effort:** 30 минут + +--- + +# ЧАСТЬ 2: Рекомендации (делаем потом) + +## Backend Security + +### VULN-006 + INFO-002 + INFO-003: Логирование и обработка ошибок + +**Комплексная задача — объединили 3 пункта.** + +**Что делаем:** + +1. **Winston для structured logs:** + ```bash + npm install winston + ``` + ```javascript + const winston = require('winston'); + const logger = winston.createLogger({ + level: 'info', + format: winston.format.json(), + transports: [ + new winston.transports.File({ filename: 'error.log', level: 'error' }), + new winston.transports.File({ filename: 'combined.log' }) + ] + }); + ``` + +2. **Sentry для алертов:** + ```bash + npm install @sentry/node + ``` + ```javascript + const Sentry = require('@sentry/node'); + Sentry.init({ dsn: process.env.SENTRY_DSN }); + ``` + +3. **Audit Trail таблица:** + ```sql + CREATE TABLE audit_log ( + id SERIAL PRIMARY KEY, + user_id TEXT, + action TEXT, -- 'login', 'update_beneficiary', 'delete_user' + entity_type TEXT, -- 'beneficiary', 'user', 'order' + entity_id TEXT, + old_values JSONB, + new_values JSONB, + ip_address TEXT, + created_at TIMESTAMP DEFAULT NOW() + ); + ``` + +4. **Generic error messages в production:** + ```javascript + app.use((err, req, res, next) => { + logger.error(err.stack); + Sentry.captureException(err); + + res.status(500).json({ + error: process.env.NODE_ENV === 'production' + ? 'Internal server error' + : err.message + }); + }); + ``` + +**Effort:** 1 день + +--- + +### VULN-009: Admin API 2FA + +**Статус:** В рекомендациях (не срочно) + +**Проблема:** Admin routes только JWT + role check, нет 2FA. + +**Fix (когда понадобится):** Добавить TOTP через `speakeasy` или email code для admin операций. + +--- + +### VULN-010: CORS без Origin + +**Статус:** В рекомендациях + +**Fix:** Настроить CORS только для разрешённых доменов: +```javascript +const corsOptions = { + origin: ['https://wellnuo.smartlaunchhub.com', 'exp://'], + credentials: true +}; +``` + +--- + +### VULN-011: Rate Limiting настройка + +**Статус:** В рекомендациях (связано с VULN-004) + +**Рекомендуемые лимиты:** +| Endpoint | Лимит | +|----------|-------| +| `/verify-otp` | 5/15min | +| `/send-otp` | 3/15min | +| Остальные | 100/15min | + +--- + +### VULN-012: Request Size Limit + +**Статус:** В рекомендациях + +**Fix:** +```javascript +app.use(express.json({ limit: '1mb' })); +``` + +--- + +### VULN-013: Sensitive Data в логах + +**Статус:** В рекомендациях (решится с Winston) + +--- + +### VULN-015: CSRF Protection + +**Статус:** В рекомендациях + +**Fix:** Для web версии добавить `csurf` middleware или использовать SameSite cookies. + +--- + +### VULN-017: Short-lived JWT + Refresh Tokens + +**Статус:** В рекомендациях + +**Текущее:** JWT живёт 7 дней, refresh токена нет. + +**Рекомендуемое:** +1. Access token: 15-60 минут +2. Refresh token: 7 дней +3. При истечении access token — автоматический refresh + +**Реализация:** +```javascript +// Генерация токенов +const accessToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: '15m' }); +const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' }); + +// Сохранить refresh token в БД +await supabase.from('refresh_tokens').insert({ user_id: userId, token: refreshToken }); + +// Endpoint для refresh +router.post('/refresh', async (req, res) => { + const { refreshToken } = req.body; + // Проверить в БД, сгенерировать новый access token +}); +``` + +**Effort:** 1 неделя (backend + frontend) + +--- + +## Frontend Security + +### FE-001: Stripe Key в Env + +**Статус:** В рекомендациях + +**Fix:** Вынести в `EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY` + +--- + +### FE-003: Legacy Credentials + +**Статус:** В рекомендациях + +**Проблема:** Hardcoded `anandk` / `anandk_8` в `services/api.ts:1444-1446` + +**Fix:** Перенести на backend proxy или убрать если не используется. + +--- + +### FE-004 + FE-008: SSL Pinning (объединено) + +**Статус:** В рекомендациях + +**Что делаем:** +1. Удалить `app/(tabs)/bug.tsx` +2. Добавить SSL pinning для API: + ```bash + npm install react-native-ssl-pinning + ``` +3. Проверка URL в WebView: + ```javascript + if (!url.startsWith('https://wellnuo.smartlaunchhub.com')) { + return false; + } + ``` + +--- + +### FE-006: Console.log Removal + +**Статус:** В рекомендациях + +**Fix:** Добавить babel plugin для production: +```bash +npm install babel-plugin-transform-remove-console --save-dev +``` + +```javascript +// babel.config.js +module.exports = { + presets: ['babel-preset-expo'], + env: { + production: { + plugins: ['transform-remove-console'] + } + } +}; +``` + +--- + +### FE-007: JWT Expiration Check + +**Статус:** В рекомендациях + +**Fix:** Проверять токен перед запросами: +```javascript +function isTokenExpired(token) { + const payload = JSON.parse(atob(token.split('.')[1])); + return payload.exp * 1000 < Date.now(); +} +``` + +--- + +### FE-010: SecureStore для Sensitive Data + +**Статус:** В рекомендациях + +**Fix:** Заменить AsyncStorage на SecureStore для: +- Tokens +- User credentials +- Любые sensitive данные + +--- + +### FE-013: Developer Mode в Production + +**Статус:** В рекомендациях + +**Fix:** Проверить и убрать dev features из production build. + +--- + +### FE-014: Deep Links Validation + +**Статус:** В рекомендациях + +**Fix:** Валидировать параметры deep links: +```javascript +function handleDeepLink(url) { + const parsed = Linking.parse(url); + if (!isValidRoute(parsed.path)) return; + if (!isValidParams(parsed.queryParams)) return; + // ... обработка +} +``` + +--- + +### FE-017: Biometric Auth + +**Статус:** В рекомендациях + +**Fix:** Добавить Face ID / Touch ID: +```bash +npx expo install expo-local-authentication +``` + +```javascript +import * as LocalAuthentication from 'expo-local-authentication'; + +const authenticate = async () => { + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Authenticate to access WellNuo', + fallbackLabel: 'Use passcode' + }); + return result.success; +}; +``` + +--- + +### FE-019: Client-Side Rate Limiting + +**Статус:** В рекомендациях + +**Fix:** Добавить debounce на кнопки: +```javascript +import { debounce } from 'lodash'; + +const handleSubmit = debounce(async () => { + // ... отправка +}, 1000, { leading: true, trailing: false }); +``` + +--- + +## Code Quality Bugs + +### BUG-001: Race Condition in AuthContext + +**Файл:** `contexts/AuthContext.tsx:44-47` + +**Fix:** Исправить зависимости useEffect, использовать useCallback. + +--- + +### BUG-002: Stale Closure in addLocalBeneficiary + +**Файл:** `contexts/BeneficiaryContext.tsx:59-85` + +**Fix:** Использовать functional update: +```javascript +setLocalBeneficiaries(prev => [...prev, newBeneficiary]); +``` + +--- + +### BUG-003: Silent Auth Failures + +**Файл:** `services/api.ts:316-384` + +**Fix:** Не маскировать ошибки — правильно обрабатывать 401. + +--- + +### BUG-004: Missing Dependencies in loadBeneficiary + +**Файл:** `app/(tabs)/beneficiaries/[id]/index.tsx:146-196` + +**Fix:** Добавить `id` в зависимости useEffect. + +--- + +### BUG-005: Memory Leak — Interval Not Cleaned + +**Файл:** `app/(tabs)/beneficiaries/[id]/index.tsx:107-144` + +**Fix:** +```javascript +useEffect(() => { + const interval = setInterval(loadData, 30 * 60 * 1000); + return () => clearInterval(interval); // cleanup! +}, []); +``` + +--- + +### LOGIC-001 — LOGIC-002, RACE-001 — RACE-002, STATE-001, ERROR-001, NULL-001 + +**Статус:** В рекомендациях — исправить при рефакторинге. + +--- + +### SMELL-001 — SMELL-006: Code Smells + +**Статус:** В рекомендациях — исправить при рефакторинге: +- Duplicate Navigation Logic +- Magic Numbers → Constants +- Mixed Auth Token Types +- Excessive `any` Types (40+) +- Callback Hell → async/await +- Silent Fallbacks → add logging + +--- + +### OPT-001 — OPT-002: Performance + +- Добавить useMemo для дорогих вычислений +- Убрать лишний JSON parsing каждый render + +--- + +### CLEAN-001 — CLEAN-002: Cleanup + +- Удалить закомментированный код +- Удалить unused imports + +--- + +### TYPE-001, TEST-001, DOC-001 + +- Добавить input validation на фронте +- Добавить тесты для error states +- Улучшить имена переменных + +--- + +## App Store Compliance + +### PERMISSION-001 + PERMISSION-002: Permission Descriptions + +**Статус:** В рекомендациях + +**Fix:** Обновить в `app.json`: +```json +{ + "expo": { + "ios": { + "infoPlist": { + "NSCameraUsageDescription": "WellNuo needs camera access to take photos for beneficiary profiles", + "NSPhotoLibraryUsageDescription": "WellNuo needs photo library access to select profile pictures for your loved ones", + "NSMicrophoneUsageDescription": "WellNuo uses microphone for voice conversations with AI assistant about your loved one's wellbeing" + } + } + } +} +``` + +--- + +### DATA-001: Account Deletion (Anonymization) + +**Статус:** В рекомендациях + +**Подход:** Обезличивание данных (не полное удаление): + +```javascript +// Backend endpoint +router.delete('/auth/account', async (req, res) => { + const userId = req.user.id; + + await supabase.from('users').update({ + email: `deleted_${userId}@anonymized.local`, + first_name: null, + last_name: null, + phone: null, + avatar_url: null, + deleted_at: new Date().toISOString() + }).eq('id', userId); + + // Удалить токены + await supabase.from('refresh_tokens').delete().eq('user_id', userId); + + res.json({ success: true }); +}); +``` + +--- + +### PRIVACY-001: Privacy Policy URL + +**Статус:** В рекомендациях + +**Fix:** Добавить в `app.json`: +```json +{ + "expo": { + "ios": { + "privacyManifests": { + "NSPrivacyAccessedAPITypes": [] + } + } + } +} +``` + +--- + +### PRIVACY-002: Privacy Manifest (iOS 17+) + +**Статус:** В рекомендациях + +**Fix:** Создать/заполнить `ios/WellNuo/PrivacyInfo.xcprivacy` + +--- + +### BACKGROUND-001: Background Modes Justification + +**Статус:** В рекомендациях + +**Что объяснить Apple:** +- **BLE background** — подключение к сенсорам WellNuo для мониторинга здоровья +- **Audio background** — голосовое общение с AI ассистентом + +--- + +### CONTENT-001: AI Chat Consent + +**Статус:** В рекомендациях + +**Fix:** Добавить consent dialog перед первым использованием AI чата. + +--- + +### TEST-001: Test Features in Production + +**Статус:** В рекомендациях + +**Fix:** Убедиться что тестовые фичи скрыты в production. + +--- + +### UX-001: Demo Mode Handling + +**Статус:** В рекомендациях + +**Fix:** Улучшить UX для demo режима. + +--- + +### MARKETING-001: App Store Materials + +**Статус:** В рекомендациях + +**Что подготовить:** +- Screenshots для всех размеров +- App description +- Keywords +- What's New text + +--- + +# ЧАСТЬ 3: Приоритеты + +## Сейчас (перед релизом) + +| # | Задача | Effort | +|---|--------|--------| +| 1 | VULN-001: Stripe webhook required | 1h | +| 2 | VULN-003: JWT Secret validation | 30m | +| 3 | VULN-004: OTP rate limiting | 2h | +| 4 | VULN-005: Input validation | 4h | +| 5 | VULN-007: Doppler secrets | 3h | +| 6 | VULN-008: npm audit fix | 30m | + +**Общее время:** ~11 часов + +## После релиза (по приоритету) + +1. **RLS (Row Level Security)** — полная переделка защиты данных +2. **Логирование** (Winston + Sentry + Audit Trail) +3. **SSL Pinning** для API и WebView +4. **Bug fixes** (race conditions, memory leaks) +5. **Account deletion** (anonymization) +6. **Biometric auth** +7. **Refresh tokens** +8. **Code quality** improvements + +--- + +# ЧАСТЬ 4: RLS (Row Level Security) — Полная реализация + +## Что это и зачем + +**Текущая защита:** Проверки доступа в коде backend. Если баг в коде — данные утекут. + +**С RLS:** Защита на уровне базы данных. Даже при баге в коде, SQL injection, или прямом доступе к БД — пользователь увидит только свои данные. + +**Аналогия:** Сейчас охранник проверяет пропуск на входе. С RLS — каждая дверь открывается только твоим ключом. + +--- + +## План реализации + +### Этап 1: Подготовка (2-3 часа) + +#### 1.1 Создать роль для приложения + +```sql +-- Создать роль для backend приложения +CREATE ROLE wellnuo_app LOGIN PASSWORD 'strong_password_here'; + +-- Дать права на таблицы +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wellnuo_app; +GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO wellnuo_app; +``` + +#### 1.2 Включить RLS на всех таблицах с sensitive данными + +```sql +-- Основные таблицы +ALTER TABLE beneficiaries ENABLE ROW LEVEL SECURITY; +ALTER TABLE user_access ENABLE ROW LEVEL SECURITY; +ALTER TABLE devices ENABLE ROW LEVEL SECURITY; +ALTER TABLE orders ENABLE ROW LEVEL SECURITY; +ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY; +ALTER TABLE deployments ENABLE ROW LEVEL SECURITY; +ALTER TABLE invitations ENABLE ROW LEVEL SECURITY; + +-- Принудительно применять RLS даже для владельца таблицы +ALTER TABLE beneficiaries FORCE ROW LEVEL SECURITY; +ALTER TABLE user_access FORCE ROW LEVEL SECURITY; +ALTER TABLE devices FORCE ROW LEVEL SECURITY; +ALTER TABLE orders FORCE ROW LEVEL SECURITY; +ALTER TABLE subscriptions FORCE ROW LEVEL SECURITY; +ALTER TABLE deployments FORCE ROW LEVEL SECURITY; +ALTER TABLE invitations FORCE ROW LEVEL SECURITY; +``` + +--- + +### Этап 2: Создание политик (4-6 часов) + +#### 2.1 Политика для beneficiaries + +```sql +-- SELECT: пользователь видит только beneficiaries к которым имеет доступ +CREATE POLICY beneficiaries_select ON beneficiaries + FOR SELECT + USING ( + id IN ( + SELECT beneficiary_id FROM user_access + WHERE accessor_id = current_setting('app.current_user_id', true) + ) + ); + +-- UPDATE: только custodian может редактировать +CREATE POLICY beneficiaries_update ON beneficiaries + FOR UPDATE + USING ( + id IN ( + SELECT beneficiary_id FROM user_access + WHERE accessor_id = current_setting('app.current_user_id', true) + AND role = 'custodian' + ) + ); + +-- DELETE: только custodian может удалять +CREATE POLICY beneficiaries_delete ON beneficiaries + FOR DELETE + USING ( + id IN ( + SELECT beneficiary_id FROM user_access + WHERE accessor_id = current_setting('app.current_user_id', true) + AND role = 'custodian' + ) + ); + +-- INSERT: любой авторизованный пользователь может создать +CREATE POLICY beneficiaries_insert ON beneficiaries + FOR INSERT + WITH CHECK ( + current_setting('app.current_user_id', true) IS NOT NULL + ); +``` + +#### 2.2 Политика для user_access + +```sql +-- SELECT: видеть записи где ты accessor или beneficiary +CREATE POLICY user_access_select ON user_access + FOR SELECT + USING ( + accessor_id = current_setting('app.current_user_id', true) + OR beneficiary_id IN ( + SELECT beneficiary_id FROM user_access + WHERE accessor_id = current_setting('app.current_user_id', true) + AND role = 'custodian' + ) + ); + +-- INSERT: только custodian может добавлять доступ +CREATE POLICY user_access_insert ON user_access + FOR INSERT + WITH CHECK ( + beneficiary_id IN ( + SELECT beneficiary_id FROM user_access + WHERE accessor_id = current_setting('app.current_user_id', true) + AND role = 'custodian' + ) + ); + +-- DELETE: только custodian может удалять доступ +CREATE POLICY user_access_delete ON user_access + FOR DELETE + USING ( + beneficiary_id IN ( + SELECT beneficiary_id FROM user_access + WHERE accessor_id = current_setting('app.current_user_id', true) + AND role = 'custodian' + ) + ); +``` + +#### 2.3 Политика для devices + +```sql +-- Устройства видны тем, кто имеет доступ к beneficiary +CREATE POLICY devices_select ON devices + FOR SELECT + USING ( + beneficiary_id IN ( + SELECT beneficiary_id FROM user_access + WHERE accessor_id = current_setting('app.current_user_id', true) + ) + ); +``` + +#### 2.4 Политика для orders + +```sql +-- Заказы видны только владельцу +CREATE POLICY orders_select ON orders + FOR SELECT + USING ( + user_id = current_setting('app.current_user_id', true) + ); +``` + +#### 2.5 Политика для users (собственный профиль) + +```sql +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE users FORCE ROW LEVEL SECURITY; + +-- Пользователь видит только себя +CREATE POLICY users_select ON users + FOR SELECT + USING ( + id = current_setting('app.current_user_id', true) + ); + +-- Пользователь редактирует только себя +CREATE POLICY users_update ON users + FOR UPDATE + USING ( + id = current_setting('app.current_user_id', true) + ); +``` + +--- + +### Этап 3: Переделка Query Builder (1-2 дня) + +#### 3.1 Новый database.js с поддержкой RLS + +```javascript +// backend/src/config/database.js +const { Pool } = require('pg'); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: 'wellnuo_app', // Используем роль с RLS + password: process.env.DB_PASSWORD, + ssl: { rejectUnauthorized: false } +}); + +/** + * Выполнить запрос от имени пользователя (с RLS) + */ +async function queryAsUser(userId, queryText, params = []) { + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + // Установить текущего пользователя для RLS + await client.query( + `SET LOCAL app.current_user_id = $1`, + [userId] + ); + + // Выполнить основной запрос + const result = await client.query(queryText, params); + + await client.query('COMMIT'); + return result; + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +/** + * Транзакция от имени пользователя + */ +async function transactionAsUser(userId, callback) { + const client = await pool.connect(); + try { + await client.query('BEGIN'); + await client.query(`SET LOCAL app.current_user_id = $1`, [userId]); + + const result = await callback(client); + + await client.query('COMMIT'); + return result; + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +module.exports = { pool, queryAsUser, transactionAsUser }; +``` + +#### 3.2 Обновлённый Query Builder + +```javascript +// backend/src/config/supabase.js (переименовать в db.js) +const { queryAsUser, transactionAsUser } = require('./database'); + +class QueryBuilder { + constructor(tableName, userId) { + this.tableName = tableName; + this.userId = userId; // Обязательно передавать userId + this._select = '*'; + this._where = []; + this._whereParams = []; + // ... остальные поля + } + + // ... существующие методы (select, eq, etc.) + + async execute() { + const sql = this._buildQuery(); + + // Все запросы идут через RLS + const result = await queryAsUser( + this.userId, + sql, + this._whereParams + ); + + return { + data: this._single ? result.rows[0] : result.rows, + error: null, + count: this._count ? result.rowCount : undefined + }; + } +} + +// Фабрика с привязкой к пользователю +function createDb(userId) { + return { + from: (tableName) => new QueryBuilder(tableName, userId) + }; +} + +module.exports = { createDb }; +``` + +--- + +### Этап 4: Обновление всех routes (2-3 дня) + +#### 4.1 Пример: beneficiaries.js ДО + +```javascript +// Старый код +const { supabase } = require('../config/supabase'); + +router.get('/me/beneficiaries', auth, async (req, res) => { + const { data, error } = await supabase + .from('beneficiaries') + .select('*'); + + // Ручная фильтрация по доступу + const accessible = data.filter(b => hasAccess(req.user.id, b.id)); + res.json(accessible); +}); +``` + +#### 4.2 Пример: beneficiaries.js ПОСЛЕ + +```javascript +// Новый код с RLS +const { createDb } = require('../config/db'); + +router.get('/me/beneficiaries', auth, async (req, res) => { + const db = createDb(req.user.id); // Привязка к пользователю + + const { data, error } = await db + .from('beneficiaries') + .select('*'); + + // RLS автоматически вернёт только доступные! + res.json(data); +}); +``` + +#### 4.3 Файлы для обновления + +``` +backend/src/routes/ +├── admin.js # Отдельная логика для админов +├── auth.js # Без RLS (регистрация/логин) +├── beneficiaries.js # ✅ RLS +├── deployments.js # ✅ RLS +├── invitations.js # ✅ RLS +├── notification-settings.js # ✅ RLS +├── orders.js # ✅ RLS +├── push-tokens.js # ✅ RLS +├── stripe.js # ✅ RLS +└── webhook.js # Без RLS (Stripe callbacks) + +backend/src/controllers/ +├── alarm.js # ✅ RLS +├── auth.js # Частично (profile = RLS) +├── beneficiary.js # ✅ RLS +├── caretaker.js # ✅ RLS +├── dashboard.js # ✅ RLS +├── deployment.js # ✅ RLS +├── device.js # ✅ RLS +├── sensor.js # ✅ RLS +└── voice.js # ✅ RLS +``` + +--- + +### Этап 5: Тестирование (1 день) + +#### 5.1 Unit тесты для RLS + +```javascript +describe('RLS Policies', () => { + it('user can only see own beneficiaries', async () => { + const db = createDb('user-1'); + const { data } = await db.from('beneficiaries').select('*'); + + // Все возвращённые beneficiaries должны быть доступны user-1 + for (const b of data) { + const hasAccess = await checkAccess('user-1', b.id); + expect(hasAccess).toBe(true); + } + }); + + it('user cannot see other users beneficiaries', async () => { + const db = createDb('user-1'); + const { data } = await db + .from('beneficiaries') + .select('*') + .eq('id', 'beneficiary-of-user-2'); + + expect(data).toHaveLength(0); // RLS блокирует + }); + + it('only custodian can update beneficiary', async () => { + const db = createDb('guardian-user'); // не custodian + + const { error } = await db + .from('beneficiaries') + .update({ name: 'Hacked' }) + .eq('id', 'some-beneficiary'); + + expect(error).toBeTruthy(); // RLS блокирует + }); +}); +``` + +#### 5.2 Интеграционные тесты + +```javascript +describe('API with RLS', () => { + it('GET /me/beneficiaries returns only accessible', async () => { + const res = await request(app) + .get('/api/me/beneficiaries') + .set('Authorization', `Bearer ${user1Token}`); + + expect(res.status).toBe(200); + // Проверить что все beneficiaries принадлежат user1 + }); + + it('cannot access other user beneficiary by ID', async () => { + const res = await request(app) + .get('/api/me/beneficiaries/other-user-beneficiary-id') + .set('Authorization', `Bearer ${user1Token}`); + + expect(res.status).toBe(404); // RLS скрывает + }); +}); +``` + +--- + +### Этап 6: Миграция и деплой (2-3 часа) + +#### 6.1 Миграция + +```sql +-- migrations/010_enable_rls.sql + +-- 1. Создать роль +CREATE ROLE wellnuo_app LOGIN PASSWORD 'xxx'; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wellnuo_app; +GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO wellnuo_app; + +-- 2. Включить RLS +ALTER TABLE beneficiaries ENABLE ROW LEVEL SECURITY; +ALTER TABLE beneficiaries FORCE ROW LEVEL SECURITY; +-- ... остальные таблицы + +-- 3. Создать политики +CREATE POLICY beneficiaries_select ON beneficiaries FOR SELECT USING (...); +-- ... остальные политики +``` + +#### 6.2 Деплой + +```bash +# 1. Применить миграцию +ssh root@91.98.205.156 +cd /var/www/wellnuo-api +node run-migration.js + +# 2. Обновить код +git pull origin main + +# 3. Обновить .env (новый пароль для wellnuo_app) +# DB_USER=wellnuo_app +# DB_PASSWORD=new_password + +# 4. Рестарт +pm2 restart wellnuo-api + +# 5. Проверить логи +pm2 logs wellnuo-api --lines 50 +``` + +--- + +## Результат + +После внедрения RLS: + +| Сценарий | Без RLS | С RLS | +|----------|---------|-------| +| Баг в коде пропускает проверку | ❌ Данные утекают | ✅ БД блокирует | +| SQL injection | ❌ Доступ ко всему | ✅ Только свои данные | +| Прямой доступ к БД | ❌ Всё видно | ✅ Только свои данные | +| Забыли добавить проверку в новом endpoint | ❌ Дыра | ✅ RLS защищает | + +--- + +## Временные затраты + +| Этап | Время | +|------|-------| +| 1. Подготовка (роли, включение RLS) | 2-3 часа | +| 2. Создание политик | 4-6 часов | +| 3. Переделка Query Builder | 1-2 дня | +| 4. Обновление routes/controllers | 2-3 дня | +| 5. Тестирование | 1 день | +| 6. Миграция и деплой | 2-3 часа | +| **ИТОГО** | **1-2 недели** | + +--- + +## Чеклист готовности + +- [ ] Роль `wellnuo_app` создана +- [ ] RLS включен на всех таблицах +- [ ] Политики созданы и протестированы +- [ ] Query Builder переделан +- [ ] Все routes используют `createDb(userId)` +- [ ] Unit тесты пройдены +- [ ] Интеграционные тесты пройдены +- [ ] Миграция применена на проде +- [ ] Код задеплоен +- [ ] Мониторинг ошибок настроен + +--- + +**Файл:** `/Users/sergei/Desktop/WellNuo/AUDIT_REPORT.md` +**Обновлён:** 2026-01-22 diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..16b71f0 --- /dev/null +++ b/PRD.md @@ -0,0 +1,150 @@ +# PRD — Персонализированные имена beneficiaries + +## Цель +Позволить каждому пользователю иметь своё персональное имя для каждого beneficiary. Custodian редактирует оригинальное имя (видно всем по умолчанию), остальные роли — своё `custom_name`. + +## Контекст +Сейчас имя beneficiary хранится в `beneficiaries.name` и одинаково для всех пользователей. Нужно добавить возможность персонализации: каждый accessor (кроме custodian) может задать своё имя через `user_access.custom_name`. + +## User Flow + +### Flow 1: Custodian редактирует имя (оригинал) + +| # | Актор | Действие | Система | Результат | +|---|-------|----------|---------|-----------| +| 1 | Custodian | Открывает список beneficiaries | GET `/me/beneficiaries` | Показывает `name` из `beneficiaries` таблицы | +| 2 | Custodian | Нажимает на beneficiary | GET `/me/beneficiaries/:id` | Открывает детали | +| 3 | Custodian | Нажимает "Edit" | — | Открывает Edit модал | +| 4 | Custodian | Меняет имя, нажимает "Save" | PATCH `/me/beneficiaries/:id` | Обновляет `beneficiaries.name` | +| 5 | System | — | Сохраняет в БД | Имя обновлено для ВСЕХ | + +### Flow 2: Guardian/Caretaker редактирует имя (персональное) + +| # | Актор | Действие | Система | Результат | +|---|-------|----------|---------|-----------| +| 1 | Caretaker | Открывает список beneficiaries | GET `/me/beneficiaries` | Показывает `custom_name` || `name` | +| 2 | Caretaker | Нажимает на beneficiary | GET `/me/beneficiaries/:id` | Открывает детали | +| 3 | Caretaker | Нажимает "Edit" | — | Открывает Edit модал | +| 4 | Caretaker | Меняет имя, нажимает "Save" | PATCH `/me/beneficiaries/:id` | Обновляет `user_access.custom_name` | +| 5 | System | — | Сохраняет в БД | Имя видно только ЭТОМУ пользователю | + +### Flow 3: Отображение (все роли) + +| # | Актор | Действие | Система | Результат | +|---|-------|----------|---------|-----------| +| 1 | User | Открывает Dashboard/список | GET `/me/beneficiaries` | — | +| 2 | System | — | Для каждого: `custom_name \|\| name` | Возвращает `displayName` | +| 3 | User | Видит список | — | Каждый beneficiary показан с персональным именем | + +--- + +## Задачи + +### Backend + +- [x] **Migration: добавить custom_name в user_access** + - Путь: `backend/migrations/009_add_custom_name.sql` + - SQL: `ALTER TABLE user_access ADD COLUMN custom_name VARCHAR(200);` + - Индекс не нужен (поле не для поиска) + +- [x] **API: изменить GET /me/beneficiaries (список)** + - Файл: `backend/src/routes/beneficiaries.js` + - В SELECT добавить `custom_name` из `user_access` + - В ответе добавить поле `displayName`: `custom_name || name` + - Также вернуть `originalName` (из `beneficiaries.name`) для UI + +- [x] **API: изменить GET /me/beneficiaries/:id (детали)** + - Файл: `backend/src/routes/beneficiaries.js` + - Добавить `custom_name` из `user_access` в SELECT + - В ответе: `displayName`, `originalName`, `customName` + +- [x] **API: изменить PATCH /me/beneficiaries/:id (обновление)** + - Файл: `backend/src/routes/beneficiaries.js` + - Логика: + - Если `role === 'custodian'` → обновить `beneficiaries.name` + - Иначе → обновить `user_access.custom_name` + - Добавить параметр `customName` в body + +- [x] **Деплой миграции на сервер** + - SSH: `root@91.98.205.156` + - Путь: `/var/www/wellnuo-api/` + - Команда: `node run-migration.js` + - PM2: `pm2 restart wellnuo-api` + +### Frontend + +- [x] **Types: обновить Beneficiary interface** + - Файл: `types/index.ts` или где определён тип + - Добавить: `displayName?: string`, `originalName?: string`, `customName?: string` + +- [x] **API service: обновить типы ответов** + - Файл: `services/api.ts` + - Обновить интерфейсы для beneficiary endpoints + +- [x] **UI: список beneficiaries — показывать displayName** + - Файл: `app/(tabs)/index.tsx` или где рендерится список + - Заменить `beneficiary.name` на `beneficiary.displayName || beneficiary.name` + +- [x] **UI: header в BeneficiaryDetail — показывать displayName** + - Файл: `app/(tabs)/beneficiaries/[id]/index.tsx` + - Строка 378: `{beneficiary.name}` → `{beneficiary.displayName || beneficiary.name}` + +- [x] **UI: Edit модал — разная логика для ролей** + - Файл: `app/(tabs)/beneficiaries/[id]/index.tsx` + - Для custodian: + - Label: "Name" + - Редактирует `name` (оригинал) + - Для guardian/caretaker: + - Label: "Your name for [originalName]" + - Placeholder: originalName + - Редактирует `customName` + - При сохранении отправлять правильное поле + +- [x] **UI: MockDashboard — показывать displayName** + - Файл: `components/MockDashboard.tsx` + - Передавать `displayName` вместо `name` + +--- + +## Вне scope (не делаем) + +- Синхронизация имён с голосовым AI (Ultravox) — будет отдельной задачей +- Интеграция с WellNuo Lite — пока не трогаем +- Миграция существующих данных — `custom_name` изначально NULL, fallback работает + +--- + +## Чеклист верификации + +### Функциональность +- [x] Custodian может редактировать оригинальное имя (`beneficiaries.name`) +- [x] Guardian/Caretaker могут редактировать своё персональное имя (`user_access.custom_name`) +- [x] Список beneficiaries показывает `displayName` (custom_name || name) +- [x] Header на детальной странице показывает `displayName` +- [x] Edit модал показывает разные labels для разных ролей +- [x] При первом открытии (custom_name = NULL) показывается оригинальное имя + +### Backend +- [x] Миграция применена без ошибок +- [x] GET `/me/beneficiaries` возвращает `displayName`, `originalName` +- [x] GET `/me/beneficiaries/:id` возвращает `displayName`, `originalName`, `customName` +- [x] PATCH `/me/beneficiaries/:id` правильно определяет что обновлять по роли + +### Код +- [x] Нет TypeScript ошибок (`npx tsc --noEmit`) +- [x] Backend работает без ошибок в логах PM2 +- [x] Нет console.log в продакшн коде (кроме отладочных с `[DEBUG]`) + +### UI/UX +- [x] Имена отображаются корректно во всех местах +- [x] Edit модал понятен для обоих типов редактирования +- [x] Нет визуальных багов + +### Edge Cases +- [x] custom_name = NULL → показывается originalName +- [x] Пустая строка custom_name = "" → считается как NULL +- [x] Длинные имена не ломают UI + +--- + +**Минимальный проходной балл: 8/10**