Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
branches:
- main # Запускать при пуше в ветку dev

concurrency:
group: deploy-main
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest # Использовать последнюю Ubuntu в качестве среды выполнения
Expand All @@ -24,7 +28,36 @@ jobs:
- name: Install dependencies # Шаг 3: Устанавливаем зависимости
run: npm ci # Используем 'ci' для быстрой и чистой установки по lock-файлу

- name: Lint
run: npm run lint

- name: Type check
run: npm run type-check

- name: Test
run: npm test

- name: Verify and deploy database migrations
env:
DIRECT_DATABASE_URL: ${{ secrets.DIRECT_DATABASE_URL }}
POSTGRES_URL_NON_POOLING: ${{ secrets.POSTGRES_URL_NON_POOLING }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
PRISMA_DATABASE_URL: ${{ secrets.PRISMA_DATABASE_URL }}
run: |
npm run migrations:verify
npx prisma generate
npm run database:verify-identity
npx prisma migrate deploy
npm run database:verify-identity

- name: Build Next.js app # Шаг 4: Собираем Next.js приложение
env:
DIRECT_DATABASE_URL: ${{ secrets.DIRECT_DATABASE_URL }}
POSTGRES_URL_NON_POOLING: ${{ secrets.POSTGRES_URL_NON_POOLING }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
PRISMA_DATABASE_URL: ${{ secrets.PRISMA_DATABASE_URL }}
run: npm run build # Эта команда должна использовать 'standalone' output из next.config.js

- name: Deploy to Server via SSH # Шаг 5: Деплоим файлы на сервер
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ npm run dev
| `DATABASE_URL` | PostgreSQL connection string (full URL with credentials) |
| `POSTGRES_URL` | Alternative Postgres connection string (used by Vercel Postgres) |
| `PRISMA_DATABASE_URL` | Explicit Prisma database URL (overrides `DATABASE_URL` if set) |
| `ADMIN_EMAIL` | Email address to grant admin access on first login (e.g., your@email.com) |
| `DIRECT_DATABASE_URL` | Direct PostgreSQL URL for migrations, backup and restore |
| `POSTGRES_URL_NON_POOLING` | Alternative direct PostgreSQL URL used when `DIRECT_DATABASE_URL` is absent |
| `ADMIN_EMAIL` | Existing administrator email used for operational notifications |
| `CONSENT_HMAC_KEY` | Dedicated 32-byte secret (64 HEX) for consent signing and verification; required in production |

#### Email & Notifications
Expand Down Expand Up @@ -179,13 +181,12 @@ npm run dev

### Admin Setup

1. **Register** — Sign up with your email or Google OAuth
2. **Set ADMIN_EMAIL** — Add your email to `.env.local`:
```bash
ADMIN_EMAIL=your@email.com
```
3. **Login Again** — Sign in again and your account will automatically be elevated to `ADMIN` role
4. **Access Admin Panel** — Navigate to `/admin` (accessible only to ADMIN users)
1. **Register** — Sign up with your email or Google OAuth.
2. **Grant the role out of band** — Set the existing verified user's `role` to `ADMIN` through a trusted database administration channel. Public registration never grants administrative access.
3. **Set `ADMIN_EMAIL`** — Configure the same email if it should receive operational notifications.
4. **Access Admin Panel** — Sign in again and navigate to `/admin`.

Production deploys must provide `DIRECT_DATABASE_URL` or `POSTGRES_URL_NON_POOLING` to CI whenever `PRISMA_DATABASE_URL` or `POSTGRES_URL` is configured. `DATABASE_URL` is accepted as a direct fallback only when it is the sole runtime database URL. Before domain migrations, the deploy workflow bootstraps a service-only `DatabaseIdentity` sentinel and verifies that runtime (including Accelerate) and direct connections resolve to the same database instance.

---

Expand Down
17 changes: 9 additions & 8 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ npm run dev
| `DATABASE_URL` | Строка подключения PostgreSQL (полный URL с учётными данными) |
| `POSTGRES_URL` | Альтернативная строка подключения Postgres (используется Vercel Postgres) |
| `PRISMA_DATABASE_URL` | Явный URL базы данных Prisma (переопределяет `DATABASE_URL` если установлена) |
| `ADMIN_EMAIL` | Email-адрес для предоставления доступа администратора при первом входе (например, your@email.com) |
| `DIRECT_DATABASE_URL` | Прямой PostgreSQL URL для миграций, backup и restore |
| `POSTGRES_URL_NON_POOLING` | Альтернативный прямой URL, если `DIRECT_DATABASE_URL` не задан |
| `ADMIN_EMAIL` | Email существующего администратора для получения служебных уведомлений |
| `CONSENT_HMAC_KEY` | Отдельный 32-байтный секрет (64 HEX) для подписи и проверки согласий; обязателен в production |

#### Email и уведомления
Expand Down Expand Up @@ -180,13 +182,12 @@ npm run dev

### Настройка администратора

1. **Зарегистрируйтесь** — Создайте учётную запись с вашим email или через Google OAuth
2. **Установите ADMIN_EMAIL** — Добавьте ваш email в `.env.local`:
```bash
ADMIN_EMAIL=your@email.com
```
3. **Войдите снова** — Войдите в систему снова, и ваша учётная запись автоматически будет повышена до роли `ADMIN`
4. **Перейдите в панель администратора** — Откройте `/admin` (доступно только пользователям с ролью ADMIN)
1. **Зарегистрируйтесь** — Создайте учётную запись с вашим email или через Google OAuth.
2. **Назначьте роль вне публичного приложения** — Через доверенный канал администрирования БД задайте существующему подтверждённому пользователю роль `ADMIN`. Публичная регистрация никогда не выдаёт права администратора.
3. **Установите `ADMIN_EMAIL`** — Укажите тот же email, если на него должны приходить служебные уведомления.
4. **Перейдите в панель администратора** — Войдите снова и откройте `/admin`.

Для production deploy в CI обязателен `DIRECT_DATABASE_URL` или `POSTGRES_URL_NON_POOLING`, если настроен отдельный runtime URL через `PRISMA_DATABASE_URL` или `POSTGRES_URL`. `DATABASE_URL` разрешён как direct fallback только когда он является единственным runtime URL. До доменных миграций workflow создаёт только служебный sentinel `DatabaseIdentity` и проверяет, что runtime-подключение (включая Accelerate) и direct URL ведут к одному экземпляру БД.

---

Expand Down
82 changes: 82 additions & 0 deletions docs/google-calendar-integration.ru.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Интеграция с Google Calendar

## Реализованный поток

Приложение использует серверный OAuth 2.0 flow и минимальный scope
`https://www.googleapis.com/auth/calendar.events`. После подключения:

- будущие события расписания отправляются в основной Google Calendar;
- создание, изменение, отмена и удаление записи синхронизируются автоматически;
- внешние события подключённого календаря отображаются в расписании администратора с заголовками
из Google, а в личном кабинете клиента — только как занятое время;
- access token обновляется через offline refresh token;
- OAuth-токены хранятся в PostgreSQL в зашифрованном виде;
- кнопка «Синхронизировать сейчас» повторно отправляет будущие записи.

Каждый администратор синхронизирует только созданные им события со своим подключённым календарём.
Календарь другого администратора не используется как fallback.

Legacy-поле с приватным iCal URL оставлено только для прежнего read-only отображения занятости.
Оно не используется для отправки записей в Google.

Если Google возвращает в iCal-фиде заголовок `Busy`, это настройка приватности самого источника:
скрытый Google заголовок восстановить невозможно. Подключение через OAuth показывает доступные
этому аккаунту заголовки событий.

## Настройка Google Cloud

1. Создать или выбрать проект в Google Cloud Console.
2. Включить Google Calendar API.
3. Настроить OAuth consent screen.
4. Создать OAuth Client ID с типом `Web application`.
5. Добавить точные redirect URI:
- production: `https://<домен>/api/google-calendar/callback`;
- local: `http://localhost:3000/api/google-calendar/callback`.
6. Добавить переменные окружения:

```dotenv
GOOGLE_CALENDAR_CLIENT_ID=...
GOOGLE_CALENDAR_CLIENT_SECRET=...
NEXT_PUBLIC_APP_URL=https://<домен>
ENCRYPTION_KEY=<стабильный-секрет-для-шифрования>
```

Если уже настроен вход через Google, интеграция также использует совместимые `AUTH_GOOGLE_ID`
и `AUTH_GOOGLE_SECRET`. В Google Cloud всё равно нужно зарегистрировать отдельный callback адрес
календаря из пункта 5.

`NEXT_PUBLIC_APP_URL` должен в точности совпадать с origin зарегистрированного redirect URI.
`ENCRYPTION_KEY` нельзя менять после подключения календаря, иначе сохранённые токены невозможно
будет расшифровать.

## Деплой

Перед включением функции применить миграцию и сгенерировать Prisma Client:

```bash
npx prisma generate
npx prisma migrate deploy
```

После деплоя открыть «Админка → Расписание → Настройки», нажать «Подключить Google Calendar»
и пройти экран согласия Google. Первичная синхронизация будущих записей запускается автоматически.
В том же диалоге доступно необязательное поле секретного iCal-адреса для read-only показа
занятости из календаря.

## План обратной синхронизации Google → приложение

Обратную синхронизацию следует реализовать отдельно, чтобы не смешивать её с исходящим потоком:

1. Сохранять внешние события и выполнять initial sync через `events.list` с `singleEvents=true`,
временным диапазоном и `nextSyncToken`.
3. Подключить Google push notifications (`events.watch`) на отдельный проверяемый webhook.
4. На webhook запускать инкрементальный `events.list` по `syncToken`, а не доверять payload webhook.
5. Хранить внешние события в отдельной модели `ExternalCalendarEvent`, не смешивая их с `Event`.
6. Применить явные правила конфликтов: внешние события блокируют время, но не создают клиента,
консультацию или уведомления автоматически.
7. Исключать события приложения по `extendedProperties.private.vershkovEventId`, чтобы избежать
циклической синхронизации и дублей.
8. Обрабатывать удаление, recurring events, all-day events, смену timezone, `410 Gone` для
протухшего sync token и повторную регистрацию watch channel.
9. Добавить журнал синхронизации, ручной retry и интеграционные тесты с зафиксированными ответами
Google API.
Loading