Compare commits

...

3 Commits

Author SHA1 Message Date
VLADIMIR a797893a29 Merge pull request 'add docs' (#1) from add_docs into master
Reviewed-on: #1
2026-08-19 17:15:26 +00:00
VLADIMIR 268b359576 fix urls 2026-08-19 23:31:37 +07:00
VLADIMIR 204a20c814 add docs 2026-08-19 02:15:57 +07:00
25 changed files with 2891 additions and 500 deletions
+12
View File
@@ -39,6 +39,18 @@ goose -dir ./migrations postgres "user=postgres password=postgres host=localhost
make test
```
## Документация и юридические документы
- [`docs/TERMS.md`](docs/TERMS.md) — Пользовательское соглашение (оферта); раздаётся сервером по постоянному URL `/api/terms`;
- [`docs/PRIVACY.md`](docs/PRIVACY.md) — Политика конфиденциальности и обработки персональных данных (152-ФЗ); раздаётся сервером по постоянному URL `/api/privacy`;
- [`docs/CONSENT.md`](docs/CONSENT.md) — текст согласия на обработку ПДн для формы регистрации (URL `/api/consent`);
- [`docs/COMPLIANCE.md`](docs/COMPLIANCE.md) — чек-лист приведения сервиса в соответствие с документами.
Юридические эндпоинты:
- `POST /api/auth/signup` — регистрация; требует `accept_terms: true` и `accept_privacy: true` (факт акцепта фиксируется в таблице `user_agreements`);
- `DELETE /api/auth/delete-account` — удаление учётной записи и персональных данных (авторизован JWT, подтверждение паролем в HTTP-заголовке `X-Password`).
## Агент «Юрист по ПО» (DSH)
В каталоге [`agents/software-lawyer`](agents/software-lawyer) лежит агент-пресет для DeepSeek Harness: специализированный консультант по правовым вопросам разработки ПО (лицензии, авторские права, персональные данные 152-ФЗ/GDPR, договоры). Отвечает на основе встроенной базы знаний (`skills/software-law/references/`), без внешних API.
+78 -2
View File
@@ -115,6 +115,47 @@ service EveningDetectiveServer {
};
}
rpc DeleteAccount(DeleteAccountReq) returns (DeleteAccountRsp) {
option (google.api.http) = {
delete: "/api/auth/delete-account"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Регистрация и вход";
summary : "Удалить учётную запись и персональные данные";
description: "Удаляет учётную запись и персональные данные текущего пользователя (ст. 21 152-ФЗ). Требует заголовок X-Password с паролем учётной записи (подтверждение владельца; пароль не передаётся в URL).";
};
}
rpc GetTerms(GetTermsReq) returns (GetTermsRsp) {
option (google.api.http) = {
get: "/api/terms"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Документы";
summary: "Получить текст Пользовательского соглашения (оферты)";
};
}
rpc GetPrivacy(GetPrivacyReq) returns (GetPrivacyRsp) {
option (google.api.http) = {
get: "/api/privacy"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Документы";
summary: "Получить текст Политики конфиденциальности";
};
}
rpc GetConsent(GetConsentReq) returns (GetConsentRsp) {
option (google.api.http) = {
get: "/api/consent"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Документы";
summary: "Получить текст Согласия на обработку персональных данных";
};
}
rpc GetUsers(GetUsersReq) returns (GetUsersRsp) {
option (google.api.http) = {
get: "/api/users"
@@ -525,8 +566,10 @@ message EchoRsp {
}
message SignupReq {
string username = 1;
string email = 2;
string username = 1;
string email = 2;
bool accept_terms = 3;
bool accept_privacy = 4;
}
message SignupRsp {
@@ -562,6 +605,39 @@ message RefreshRsp {
string refreshToken = 3;
}
// Пароль подтверждает, что удаляет аккаунт его владелец, а не обладатель
// украденного access-токена (ст. 21 152-ФЗ). Передаётся HTTP-заголовком
// X-Password (пробрасывается матчером в cmd/evening_detective_server/main.go),
// чтобы креденшел не попадал в URL/query-параметры и логи.
message DeleteAccountReq {}
message DeleteAccountRsp {
string error = 1;
}
// Документы раздаются по постоянным URL, на которые ссылается сервис при
// фиксации акцептов соглашений (user_agreements) и форма регистрации.
message GetTermsReq {}
message GetTermsRsp {
string error = 1;
string text = 2;
}
message GetPrivacyReq {}
message GetPrivacyRsp {
string error = 1;
string text = 2;
}
message GetConsentReq {}
message GetConsentRsp {
string error = 1;
string text = 2;
}
message GetUsersReq {}
message GetUsersRsp {
+21 -1
View File
@@ -27,6 +27,7 @@ import (
"net"
"net/http"
"os"
"strings"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/jackc/pgx/v5/pgxpool"
@@ -125,6 +126,9 @@ func main() {
"/crabs.evening_detective_server.EveningDetectiveServer/GetScenariosCatalog": true,
"/crabs.evening_detective_server.EveningDetectiveServer/GetTeamStory": true,
"/crabs.evening_detective_server.EveningDetectiveServer/AddTeamAction": true,
"/crabs.evening_detective_server.EveningDetectiveServer/GetTerms": true,
"/crabs.evening_detective_server.EveningDetectiveServer/GetPrivacy": true,
"/crabs.evening_detective_server.EveningDetectiveServer/GetConsent": true,
},
processorJWT,
),
@@ -158,7 +162,23 @@ func main() {
log.Fatalln("Failed to dial server:", err)
}
gwmux := runtime.NewServeMux()
// X-Real-IP пробрасывается от доверенного обратного прокси (если фронт-
// контур развёрнут перед gateway, см. docs/COMPLIANCE.md п. 6.6) и
// используется в clientInfoFromContext для фиксации IP клиента при
// акцепте соглашений (ст. 9 152-ФЗ). X-Password — подтверждение паролем
// при удалении аккаунта (не в query, чтобы не светить креденшел в URL
// и логах).
gwmux := runtime.NewServeMux(
runtime.WithIncomingHeaderMatcher(func(key string) (string, bool) {
switch {
case strings.EqualFold(key, "X-Real-IP"):
return "x-real-ip", true
case strings.EqualFold(key, "X-Password"):
return "x-password", true
}
return runtime.DefaultHeaderMatcher(key)
}),
)
// Register Greeter
err = proto.RegisterEveningDetectiveServerHandler(context.Background(), gwmux, conn)
if err != nil {
@@ -16,6 +16,30 @@
"application/json"
],
"paths": {
"/api/auth/delete-account": {
"delete": {
"summary": "Удалить учётную запись и персональные данные",
"description": "Удаляет учётную запись и персональные данные текущего пользователя (ст. 21 152-ФЗ). Требует заголовок X-Password с паролем учётной записи (подтверждение владельца; пароль не передаётся в URL).",
"operationId": "EveningDetectiveServer_DeleteAccount",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverDeleteAccountRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"Регистрация и вход"
]
}
},
"/api/auth/login": {
"post": {
"summary": "Войти и получить токена доступа",
@@ -191,6 +215,29 @@
]
}
},
"/api/consent": {
"get": {
"summary": "Получить текст Согласия на обработку персональных данных",
"operationId": "EveningDetectiveServer_GetConsent",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverGetConsentRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"Документы"
]
}
},
"/api/files/upload": {
"post": {
"summary": "Загрузить файл",
@@ -632,6 +679,29 @@
]
}
},
"/api/privacy": {
"get": {
"summary": "Получить текст Политики конфиденциальности",
"operationId": "EveningDetectiveServer_GetPrivacy",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverGetPrivacyRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"Документы"
]
}
},
"/api/scenario": {
"post": {
"summary": "Создать сценарий",
@@ -1262,6 +1332,29 @@
]
}
},
"/api/terms": {
"get": {
"summary": "Получить текст Пользовательского соглашения (оферты)",
"operationId": "EveningDetectiveServer_GetTerms",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverGetTermsRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"tags": [
"Документы"
]
}
},
"/api/test/echo": {
"post": {
"summary": "Проверить обработку данных",
@@ -1758,6 +1851,14 @@
}
}
},
"evening_detective_serverDeleteAccountRsp": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"evening_detective_serverDeleteGameRsp": {
"type": "object",
"properties": {
@@ -1888,6 +1989,17 @@
}
}
},
"evening_detective_serverGetConsentRsp": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"text": {
"type": "string"
}
}
},
"evening_detective_serverGetGameRsp": {
"type": "object",
"properties": {
@@ -1954,6 +2066,17 @@
}
}
},
"evening_detective_serverGetPrivacyRsp": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"text": {
"type": "string"
}
}
},
"evening_detective_serverGetScenarioRsp": {
"type": "object",
"properties": {
@@ -1994,6 +2117,17 @@
}
}
},
"evening_detective_serverGetTermsRsp": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"text": {
"type": "string"
}
}
},
"evening_detective_serverGetUserByIdRsp": {
"type": "object",
"properties": {
@@ -2222,6 +2356,12 @@
},
"email": {
"type": "string"
},
"acceptTerms": {
"type": "boolean"
},
"acceptPrivacy": {
"type": "boolean"
}
}
},
+123
View File
@@ -0,0 +1,123 @@
# ЧЕК-ЛИСТ COMPLIANCE: ПРИВЕДЕНИЕ СЕРВИСА В СООТВЕТСТВИЕ С ДОКУМЕНТАМИ
Сервис «Вечерний детектив» · Обновлено: 18.08.2026
Сопутствующие документы: `docs/TERMS.md` (Пользовательское соглашение), `docs/PRIVACY.md` (Политика конфиденциальности), `docs/CONSENT.md` (Согласие на обработку ПДн).
Легенда: ✅ — реализовано, 🟡 — частично / требует решения владельца, ⬜ — осталось сделать.
---
## 1. Юридические документы
| № | Задача | Статус | Примечание |
|---|---|---|---|
| 1.1 | Пользовательское соглашение (оферта) | ✅ | `docs/TERMS.md` — заполнить плейсхолдеры, утвердить у юриста |
| 1.2 | Политика конфиденциальности (152-ФЗ) | ✅ | `docs/PRIVACY.md` — заполнить плейсхолдеры, утвердить у юриста |
| 1.3 | Текст согласия на обработку ПДн | ✅ | `docs/CONSENT.md` — встроить в форму регистрации |
| 1.4 | Публикация документов на сайте по постоянным URL | ✅ | RPC `GetTerms`/`GetPrivacy`/`GetConsent` (`api/main.proto`, пути `/api/terms`, `/api/privacy`, `/api/consent`); текст встраивается из `docs/embed.go`; подставить реальные домены в тексты документов |
## 2. Акцепт соглашений при регистрации (ст. 428 ГК РФ, ст. 9 152-ФЗ)
| № | Задача | Статус | Где |
|---|---|---|---|
| 2.1 | Поля `accept_terms` / `accept_privacy` в запросе регистрации | ✅ | `api/main.proto` (`SignupReq`), перегенерирован `proto/` |
| 2.2 | Серверная проверка: регистрация без акцепта отклоняется (`ErrTermsNotAccepted`) | ✅ | `internal/services/users_service/service.go``AddUser`; `internal/app/server.go``Signup` |
| 2.3 | Таблица `user_agreements` (user_id, тип, версия, дата, IP, user-agent) | ✅ | `migrations/20260818231943_create_user_agreements_table.sql` |
| 2.4 | Фиксация акцепта при регистрации (terms + privacy), атомарно с созданием пользователя | ✅ | `users_repo.AddUserWithAgreements` (одна транзакция: пользователь не существует без записей о согласиях) |
| 2.5 | Версии документов как константы (`termsVersion`, `privacyVersion`) | ✅ | `users_service/service.go` — при новой редакции документов поднять версию |
| 2.6 | Чекбоксы в форме регистрации (не предустановленные!) + блокировка отправки | ⬜ | Фронтенд (репозиторий фронта) |
| 2.7 | Возраст 18+ (текст + проверка) | 🟡 | В документах и коде — запрет; проверить, что форма запрашивает подтверждение возраста |
| 2.8 | Фиксация IP и user-agent клиента при акцепте (ст. 9 ч. 3 152-ФЗ) | ✅ | `clientInfoFromContext`: `X-Real-IP` от доверенного прокси (матчер в `cmd/evening_detective_server/main.go`), иначе последний элемент `X-Forwarded-For`; UA — `grpcgateway-user-agent` |
## 3. Удаление аккаунта и данных (ст. 14, 21 152-ФЗ)
| № | Задача | Статус | Где |
|---|---|---|---|
| 3.1 | Эндпоинт `DELETE /api/auth/delete-account` (авторизован JWT) | ✅ | `api/main.proto` (`DeleteAccount`), `internal/app/server.go` |
| 3.2 | Удаление аккаунта: ПДн удаляются (refresh-токены, учётная запись); записи об акцептах СОХРАНЯЮТСЯ обезличенными (факт/версия/время; `user_id` отвязывается через FK `ON DELETE SET NULL`, `ip`/`user_agent` обнуляются — бремя доказывания согласия на операторе, ст. 9 ч. 4 152-ФЗ) | ✅ | `internal/repos/users_repo/repo.go``DeleteUser` (транзакция); миграции `20260818231943_create_user_agreements_table.sql`, `20260818232725_keep_content_on_user_delete.sql` |
| 3.2а | Подтверждение паролем при удалении аккаунта (защита от удаления чужим обладателем украденного access-токена) | ✅ | HTTP-заголовок `X-Password` (матчер в `cmd/evening_detective_server/main.go`) → `users_service.DeleteAccount` (bcrypt-сверка); пароль не попадает в URL/query |
| 3.3 | Судьба контента при удалении аккаунта | ✅ | Выбран вариант «отчуждение исключительного права на опубликованные Сценарии»: контент сохраняется, автор не вправе требовать удаления (п. 7.2, 7.6, 7.8 TERMS; ст. 1234, 1269 ГК РФ) |
| 3.4 | NULL-safe чтение сценариев с обезличенным автором | ✅ | `scenarios_repo` (scan через указатели), `mapUser` возвращает nil → `author: null` в API (клиент показывает «Автор удалён») |
| 3.5 | Удаление файлов из S3 при удалении аккаунта | ⬜ | Сейчас удаляются только записи в БД; файлы пользователя в S3 (`file_service`) остаются — добавить вызов `fileStorage.Delete` для файлов автора |
| 3.6 | UI-кнопка «Удалить аккаунт» + подтверждение | ⬜ | Фронтенд |
| 3.7 | Отзыв согласия без удаления аккаунта (ст. 9 ч. 2, 152-ФЗ) | ⬜ | Опционально: отдельный эндпоинт «отозвать согласие» → прекращение обработки, блокировка аккаунта |
| 3.8 | Инвалидация выданных access-токенов после удаления аккаунта | 🟡 | JWT без revoke-листа: refresh-токен удаляется сразу, access-токен действует до истечения TTL (3 ч, `processor_jwt`); запросы с устаревшим токеном после удаления получат ошибку FK/«Пользователь не найден». Защита: короткий TTL; при необходимости — версия токена/чёрный список |
**Примечание по акцептам (п. 3.2):** решение владельца — хранить записи об акцептах обезличенными после удаления аккаунта (факт/версия/время без `user_id`/`ip`/`user_agent`): ст. 9 ч. 4 152-ФЗ возлагает на оператора бремя доказывания получения согласия. Альтернатива, если позже решат уничтожать и их (ст. 21 152-ФЗ): вернуть FK `ON DELETE CASCADE` в миграции `20260818231943` и `DELETE FROM user_agreements WHERE user_id = $1` в `DeleteUser`.
## 3а. Отчуждение исключительных прав на сценарии (ст. 1234, 1269 ГК РФ)
| № | Задача | Статус | Где |
|---|---|---|---|
| 3а.1 | Пункт TERMS об отчуждении исключительного права при публикации (безвозмездно, в полном объёме) | ✅ | `docs/TERMS.md` п. 7.27.4 |
| 3а.2 | Запрет автору удалять / снимать с публикации опубликованный сценарий (`ErrScenarioPublished`); ПРАВКА КОНТЕНТА опубликованного сценария РАЗРЕШЕНА владельцу (решение владельца продукта) | ✅ | `internal/services/scenarios_service/service.go`; админ — исключение; guard в SQL от гонки «чтение → запись» для status/delete |
| 3а.3 | Проверка владельца сценария при всех изменениях (public/draft/delete/update/места) (`ErrScenarioNotOwner`) | ✅ | `internal/services/scenarios_service/service.go``getScenarioForChange` |
| 3а.4 | Право на отзыв ограничено моментом обнародования (ст. 1269 ГК РФ) | ✅ | `docs/TERMS.md` п. 7.6 |
| 3а.5 | Отдельный чекбокс «передаю исключительные права» при первой публикации + серверная проверка | ⬜ | Фронтенд + бэкенд (сейчас публикация доступна владельцу без доп. подтверждения; добавить `confirm_transfer` в `PublicScenarioReq` при необходимости) |
| 3а.6 | Проверка владельца при редактировании сценария (UpdateScenario, AddScenarioPlace и др.) | ✅ | `getScenarioForChange` в `internal/services/scenarios_service/service.go`; правка опубликованного разрешена (решение владельца, см. 3а.2) |
| 3а.7 | Оценка рисков ЗоЗПП (ст. 16) для безвозмездного отчуждения в договоре присоединения | 🟡 | См. «Риски» ниже; при запуске для широкой аудитории — консультация юриста и судебная практика |
**Риски отчуждения прав в договоре присоединения:**
- **ЗоЗПП, ст. 16**: безвозмездное отчуждение исключительного права на пользовательский контент в оферте для потребителя может быть оспорено как условие, ущемляющее права потребителя. Снижение риска: отдельное явное подтверждение при публикации, возможность отозвать Сценарий до публикации (черновики не отчуждаются), прозрачное выделение условия.
- **Личные неимущественные права** (ст. 1265 ГК РФ) не отчуждаются: автор сохраняет право авторства и может требовать его признания — отчуждается только имущественное (исключительное) право.
- **Право на отзыв** (ст. 1269 ГК РФ) действует только до обнародования — после публикации требования автора об удалении не подлежат удовлетворению (кроме случаев, предусмотренных законом, напр. признание сценария нарушающим права третьих лиц).
- **Коммерческие организации**: п. 3.1 ст. 1234 ГК РФ запрещает безвозмездное отчуждение исключительного права между коммерческими организациями — не применяется, т.к. пользователи — физические лица.
## 4. Регистрация в Роскомнадзоре (ст. 22 152-ФЗ)
| № | Задача | Статус | Примечание |
|---|---|---|---|
| 4.1 | Оценить необходимость уведомления РКН о начале обработки ПДн | 🟡 | Интернет-сервис с автоматизированной обработкой обычно требует уведомления; исключения ч. 2 ст. 22 вряд ли применимы. Подать до запуска в прод |
| 4.2 | Подать уведомление (портал РКН, форма «оператор ПДн») | ⬜ | Внешняя процедура; оператор — физлицо |
| 4.3 | Получить и хранить номер уведомления / решение | ⬜ | Приложить к документации |
## 5. Локализация баз данных (ст. 18 ч. 5 152-ФЗ)
| № | Задача | Статус | Где |
|---|---|---|---|
| 5.1 | PostgreSQL в РФ | ✅ | `docker-compose-db.yml` / прод-конфигурация — проверить, что БД в РФ |
| 5.2 | S3 (RustFS) — файлы пользователей | 🟡 | Проверить, где физически находится `S3_HOST` в проде |
| 5.3 | SMTP-логи переписки | 🟡 | Проверить, не сохраняются ли письма/логи за пределами РФ |
## 6. Безопасность (ст. 19 152-ФЗ)
| № | Задача | Статус | Где |
|---|---|---|---|
| 6.1 | Хеширование паролей (bcrypt) | ✅ | `users_service/service.go` |
| 6.2 | JWT access (3 ч) + refresh (7 дн) с ротацией | ✅ | `processor_jwt` |
| 6.3 | HTTPS на проде | 🟡 | Через обратный прокси (не в репозитории) |
| 6.4 | Назначить ответственного за обработку ПДн (ст. 22.1) | ⬜ | Организационный шаг (для физлица-оператора — сам владелец) |
| 6.5 | Внутренний документ: сроки хранения логов, порядок реагирования на утечки (ст. 21 ч. 3.1 — уведомление РКН за 24 ч) | ⬜ | Организационный шаг |
| 6.6 | Gateway только за доверенным обратным прокси, перезаписывающим `X-Real-IP` (иначе клиент спуфит IP при акцепте; значения валидируются `netip.ParseAddr`) | 🟡 | Требование к прод-развёртыванию: nginx/Caddy перед `:8090`; настройка не в репозитории |
## 7. Права субъекта (ст. 14, 17, 20 152-ФЗ)
| № | Задача | Статус | Где |
|---|---|---|---|
| 7.1 | Эндпоинт «получить свои данные» (GDPR-стиль / ст. 14) | 🟡 | Частично есть `GET /api/users/me`; полная выгрузка ПДн (все таблицы) — добавить |
| 7.2 | Эндпоинт «изменить email/ник» | ⬜ | Сейчас нет; без него нельзя исполнить «уточнение ПДн» |
| 7.3 | Эндпоинт «удалить аккаунт» | ✅ | П. 3.1 |
| 7.4 | Процедура ответа на запросы (10 раб. дней, ст. 20) | ⬜ | Организационный шаг + адрес `[email]` из документов |
## 8. Прочее
| № | Задача | Статус | Примечание |
|---|---|---|---|
| 8.1 | Обновление swagger (`cmd/evening_detective_server/main.swagger.json`) | ✅ | Перегенерирован `make generate` |
| 8.2 | Прогон миграции на реальной БД (`goose up`) | ⬜ | Выполнить на staging перед продом |
| 8.3 | Интеграционный тест: signup без акцепта → ошибка; delete-account → данные удалены | ⬜ | `go test` (сейчас тестов нет) |
| 8.4 | Обновить README: новые эндпоинты, документы | ⬜ | — |
| 8.5 | GDPR (если появятся пользователи из ЕС) | ⬜ | Политика + DPA + механизмы прав субъекта — отдельный этап |
---
## Порядок действий перед запуском в прод
1. Заполнить плейсхолдеры в `docs/TERMS.md`, `docs/PRIVACY.md`, `docs/CONSENT.md` и утвердить у юриста.
2. Вставить в тексты документов реальные домены (`/api/terms`, `/api/privacy`, `/api/consent`); вставить чекбоксы акцепта, подтверждение отчуждения прав при публикации сценария и кнопку «Удалить аккаунт» во фронтенд.
3. Подать уведомление в Роскомнадзор (п. 4.2) до начала обработки.
4. Проверить локализацию: БД, S3, SMTP — в РФ.
5. Прогнать `goose up` на staging; добавить интеграционный тест (п. 8.3) — включая сценарий: публикация → удаление запрещено; удаление аккаунта → сценарий остаётся с `author: null`.
6. Решить задачу удаления файлов из S3 (п. 3.5) и «изменить email/ник» (п. 7.2) — по остаточному принципу после запуска.
+54
View File
@@ -0,0 +1,54 @@
# СОГЛАСИЕ НА ОБРАБОТКУ ПЕРСОНАЛЬНЫХ ДАННЫХ
**Сервис «Вечерний детектив»** · **Редакция от «__» ________ 20__ года**
*Настоящий документ является шаблоном и носит информационный характер. Перед использованием текст должен быть адаптирован и утверждён квалифицированным юристом.*
---
## Текст согласия (для регистрационной формы)
Я, свободно, своей волей и в своём интересе, являясь дееспособным лицом, достигшим возраста 18 лет, даю своё согласие **[ФИО Оператора]** (далее — **Оператор**), зарегистрированному по адресу: **[адрес]**, на обработку моих персональных данных (далее — **ПДн**) в соответствии с Федеральным законом от 27.07.2006 № 152-ФЗ «О персональных данных» (далее — **152-ФЗ**), а именно на совершение действий, предусмотренных п. 3 ч. 1 ст. 3 152-ФЗ, со следующими ПДн:
1. адрес электронной почты;
2. имя пользователя (никнейм / псевдоним);
3. сведения об учётной записи (включая роли и права);
4. данные об участии в играх, командах, поданных заявках;
5. создаваемые мной материалы (сценарии, тексты, иной контент);
6. загружаемые мной файлы;
7. технические данные (IP-адрес, сведения об устройстве и браузере, файлы cookie);
8. иные данные, добровольно предоставляемые мной при использовании Сервиса.
**Цели обработки** ПДн: идентификация и аутентификация при использовании Сервиса «Вечерний детектив»; предоставление доступа к Сервису и его функциям; связь со мной (направление пароля, уведомлений); обеспечение безопасности Сервиса; улучшение качества Сервиса; исполнение требований законодательства РФ.
**Перечень действий с ПДн:** сбор, запись, систематизация, накопление, хранение, уточнение (обновление, изменение), извлечение, использование, обезличивание, блокирование, удаление, уничтожение, — как с использованием средств автоматизации, так и без их использования.
Настоящее согласие действует со дня его предоставления до дня его отзыва. Согласие может быть отозвано мной в любое время путём направления Оператору письменного требования (в т.ч. в форме электронного документа) на адрес: **[email]**. При отзыве согласия Оператор обязан прекратить обработку ПДн и уничтожить их в срок, не превышающий 30 (тридцати) календарных дней с даты получения отзыва, если иное не предусмотрено законодательством РФ.
Я ознакомлен(а) с Политикой конфиденциальности и обработки персональных данных, размещённой по адресу: **[адрес политики]**, и Пользовательским соглашением, размещённым по адресу: **[адрес соглашения]**, и согласен(на) с их условиями.
«___» ________ 20___ года
Подпись: ____________________ / ____________________
---
## Техническая реализация в форме регистрации
Для юридической значимости согласие должно быть получено до начала обработки и подтверждаемо:
1. **Чекбоксы с явным согласием** (не предустановленные):
- [ ] Я принимаю условия [Пользовательского соглашения]
- [ ] Я даю согласие на обработку персональных данных в соответствии с [Политикой конфиденциальности]
2. **Блокировка отправки** формы при неотмеченных чекбоксах (клиентская и серверная проверка).
3. **Серверная проверка**: `POST /api/auth/signup` отклоняет запрос (`accept_terms=false` или `accept_privacy=false`) с ошибкой — уже реализовано в бэкенде (см. `docs/COMPLIANCE.md`).
4. **Фиксация факта акцепта**: сервер сохраняет запись в таблице `user_agreements` (тип документа, версия, дата, IP, user-agent) — уже реализовано.
5. **Возраст 18+**: подтверждение возраста включается в текст согласия и регистрационную форму.
6. **Отдельное подтверждение при публикации Сценария** (рекомендуется): при первом переводе сценария в статус «опубликован» — чекбокс «Я передаю Оператору исключительные права на этот сценарий безвозмездно в полном объёме» с указанием, что после публикации отозвать сценарий нельзя (ст. 1269 ГК РФ). Серверная проверка: `PUT /api/scenarios/{id}/public` отклоняет запрос без этого подтверждения (см. раздел «Риски» в `docs/COMPLIANCE.md`).
## Примечания
- **Отдельное согласие vs соглашение**: согласие на обработку ПДн (ст. 9 152-ФЗ) и акцепт пользовательского соглашения (ст. 428, 1286 ГК РФ) — разные юридические факты, поэтому в форме два отдельных чекбокса.
- Для несовершеннолетних (до 18 лет) использование Сервиса запрещено (п. 4.1 Пользовательского соглашения); согласие родителей не предусмотрено, т.к. доступ запрещён полностью.
- При введении платных услуг и изменении целей обработки согласие должно быть переактуализировано.
- **Удаление аккаунта и контент**: отзыв согласия / удаление Учётной записи влечёт удаление персональных данных, но не прекращает отчуждение исключительного права на опубликованные Сценарии: они остаются в Сервисе, атрибуция обезличивается (п. 7.8 Пользовательского соглашения, ст. 1234 ГК РФ).
+185
View File
@@ -0,0 +1,185 @@
# ПОЛИТИКА КОНФИДЕНЦИАЛЬНОСТИ И ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ
Сервис «Вечерний детектив»
**Редакция от «__» ________ 20__ года**
*Настоящий документ является шаблоном и носит информационный характер. Перед публикацией документ должен быть адаптирован и утверждён квалифицированным юристом.*
---
## 1. Общие положения
1.1. Настоящая Политика конфиденциальности и обработки персональных данных (далее — **Политика**) определяет порядок и условия обработки персональных данных (далее — **ПДн**) Пользователей Сервиса «Вечерний детектив» (далее — **Сервис**), размещённого в сети Интернет по адресу: **[адрес сайта]**.
1.2. Оператором ПДн (ст. 3 Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных», далее — **152-ФЗ**) является **[ФИО Оператора]** (далее — **Оператор**). Реквизиты Оператора указаны в разделе 14 Политики.
1.3. Политика разработана во исполнение требований ст. 18.1 152-ФЗ и действует в отношении всех ПДн, которые Оператор может получить о Пользователе при использовании Сервиса.
1.4. Политика является неотъемлемой частью Пользовательского соглашения (далее — **Соглашение**), размещённого по адресу: **[адрес соглашения]**. Термины, используемые в Политике, применяются в значениях, определённых Соглашением, если иное не установлено Политикой или законодательством РФ.
1.5. Используя Сервис (регистрируясь, авторизуясь, осуществляя доступ к Сервису), Пользователь выражает согласие с условиями Политики и согласие на обработку своих ПДн в порядке и на условиях, изложенных в Политике.
1.6. Если Пользователь не согласен с условиями Политики, он обязан прекратить использование Сервиса.
## 2. Состав обрабатываемых ПДн
2.1. Оператор обрабатывает следующие ПДн Пользователей:
2.1.1. **Данные, предоставляемые Пользователем при регистрации и использовании Сервиса:**
- адрес электронной почты;
- имя пользователя (никнейм / псевдоним);
- пароль (в захешированном виде, недоступном для прочтения);
- иные данные, которые Пользователь добровольно указывает при использовании Сервиса.
2.1.2. **Данные, создаваемые в процессе использования Сервиса:**
- сведения об Учётной записи и её настройках;
- сведения о ролях и правах Пользователя в Сервисе;
- данные об участии в Играх, о Командах, о поданных заявках;
- создаваемые Пользователем материалы (Сценарии, тексты, иной Пользовательский контент);
- загружаемые Пользователем файлы (изображения и иные материалы), в т.ч. при подаче заявок;
- история действий Пользователя в Сервисе (в т.ч. сведения о входе в Учётную запись).
2.1.3. **Технические данные, собираемые автоматически:**
- IP-адрес устройства;
- данные об устройстве и браузере (тип, версия, язык, разрешение экрана);
- файлы cookie и аналогичные технологии;
- сведения о времени и длительности сеансов;
- иные технические данные, передаваемые устройством Пользователя при обращении к Сервису.
2.1.4. Оператор не обрабатывает специальные категории ПДн (ст. 10 152-ФЗ: раса, национальность, политические взгляды, религиозные убеждения, состояние здоровья, интимная жизнь) и биометрические ПДн (ст. 11 152-ФЗ), за исключением случаев, прямо предусмотренных законодательством РФ.
2.1.5. Загружая файлы и размещая Пользовательский контент, Пользователь самостоятельно несёт ответственность за то, чтобы такие материалы не содержали ПДн третьих лиц без их согласия, а также иных сведений, обработка которых не допускается.
## 3. Цели обработки ПДн
3.1. Оператор обрабатывает ПДн Пользователей исключительно в следующих целях:
- идентификация и аутентификация Пользователя при регистрации и входе в Сервис;
- предоставление доступа к Сервису и его функциям;
- обеспечение функционирования Сервиса (в т.ч. создание Игр, Команд, Сценариев, обработка заявок);
- связь с Пользователем: направление пароля при регистрации, уведомлений о восстановлении пароля, иных служебных сообщений, связанных с использованием Сервиса;
- обеспечение безопасности Сервиса, защита от несанкционированного доступа, мошенничества и злоупотреблений;
- улучшение качества Сервиса, анализ использования его функций;
- исполнение требований законодательства РФ.
3.2. Оператор не использует ПДн для целей, несовместимых с целями, указанными в п. 3.1 Политики.
## 4. Правовые основания обработки ПДн
4.1. Обработка ПДн Пользователя осуществляется на следующих основаниях (ст. 6 152-ФЗ):
- **согласие Пользователя** на обработку ПДн, выраженное при регистрации и/или использовании Сервиса (акцепт Политики и Соглашения);
- **исполнение Соглашения**, стороной которого является Пользователь (в т.ч. предоставление доступа к Сервису, исполнение обязательств Оператора);
- **исполнение требований законодательства РФ**, возложенных на Оператора;
- **защита прав и законных интересов** Оператора и третьих лиц (в т.ч. предотвращение злоупотреблений и нарушений).
4.2. Согласие Пользователя на обработку ПДн является конкретным, информированным и сознательным (ст. 9 152-ФЗ). Согласие может быть отозвано Пользователем в любое время в порядке, предусмотренном разделом 12 Политики.
## 5. Порядок и условия обработки ПДн
5.1. Обработка ПДн осуществляется с использованием средств автоматизации и без использования средств автоматизации, в т.ч. путём сбора, записи, систематизации, накопления, хранения, уточнения (обновления, изменения), извлечения, использования, передачи, обезличивания, блокирования, удаления, уничтожения (ст. 3 152-ФЗ).
5.2. Обработка ПДн осуществляется с соблюдением принципов, установленных ст. 5 152-ФЗ: законности и справедливости, соответствия целям обработки, минимизации, точности и достаточности, ограничения сроков хранения.
5.3. Оператор не раскрывает ПДн Пользователей третьим лицам и не распространяет их без согласия Пользователя, за исключением случаев, предусмотренных законодательством РФ.
5.4. Для обеспечения функционирования Сервиса Оператор может привлекать третьих лиц (технических подрядчиков): поставщиков хостинга и вычислительных мощностей, почтового сервиса для отправки сообщений, сервиса облачного хранения файлов, сервисов аналитики. Передача ПДн таким лицам осуществляется в объёме, необходимом для оказания соответствующих услуг, на основании договоров, содержащих обязательства о конфиденциальности и обеспечении безопасности ПДн (ст. 6, 19 152-ФЗ).
5.5. ПДн Пользователей не передаются третьим лицам в целях маркетинга и рекламы без отдельного согласия Пользователя.
## 6. Файлы cookie и аналитика
6.1. Сервис может использовать файлы cookie и аналогичные технологии (localStorage, sessionStorage, идентификаторы устройств) для:
- обеспечения корректной работы Сервиса (технически необходимые cookie);
- запоминания настроек и предпочтений Пользователя;
- сбора статистики использования Сервиса (аналитические cookie).
6.2. Технически необходимые cookie не требуют согласия Пользователя. Использование аналитических и иных необязательных cookie осуществляется с согласия Пользователя, выраженного через настройки браузера и/или элементы интерфейса Сервиса.
6.3. Пользователь может в любой момент отключить или ограничить использование cookie в настройках браузера. Отключение cookie может повлиять на доступность отдельных функций Сервиса.
6.4. Данные аналитики могут обрабатываться в обезличенном виде и использоваться для улучшения Сервиса.
## 7. Трансграничная передача ПДн
7.1. На дату вступления Политики в силу трансграничная передача ПДн (ст. 12 152-ФЗ) Оператором не осуществляется.
7.2. В случае необходимости трансграничной передачи ПДн Оператор обеспечит соблюдение требований ст. 12 152-ФЗ, включая (при необходимости) уведомление или получение разрешения уполномоченного органа (Роскомнадзора) в соответствии с действующим законодательством РФ, и соответствующим образом актуализирует настоящую Политику.
## 8. Локализация баз данных
8.1. Оператор обеспечивает запись, систематизацию, накопление, хранение, уточнение, извлечение ПДн граждан Российской Федерации с использованием баз данных, находящихся на территории Российской Федерации (ст. 18 ч. 5 152-ФЗ).
## 9. Меры по защите ПДн
9.1. Оператор принимает правовые, организационные и технические меры, необходимые и достаточные для обеспечения безопасности ПДн (ст. 19 152-ФЗ), включая:
- назначение лица, ответственного за организацию обработки ПДн (ст. 22.1 152-ФЗ);
- применение правовых, организационных и технических мер защиты информации (шифрование паролей, ограничение и разграничение доступа к ПДн, защита каналов передачи данных, резервное копирование);
- внутренний контроль за соответствием обработки ПДн требованиям законодательства;
- оценку вреда, который может быть причинён субъектам ПДн в случае нарушения требований 152-ФЗ;
- ознакомление лиц, допущенных к обработке ПДн, с требованиями законодательства и Политики.
9.2. Пароли Пользователей хранятся в захешированном виде и недоступны для чтения никому, включая работников и подрядчиков Оператора.
9.3. При выявлении фактов неправомерной или случайной передачи (предоставления, распространения, доступа) ПДн, повлёкшей нарушение прав субъектов ПДн, Оператор в срок, не превышающий 24 часов, уведомляет уполномоченный орган по защите прав субъектов персональных данных (Роскомнадзор) о произошедшем инциденте, а также в установленных законодательством случаях уведомляет субъектов ПДн (ст. 21 ч. 3.1 152-ФЗ).
## 10. Сроки обработки и хранения ПДн
10.1. ПДн обрабатываются в течение срока, необходимого для достижения целей обработки, либо в течение срока действия Соглашения, если иное не предусмотрено законодательством РФ.
10.2. ПДн уничтожаются либо обезличиваются по достижении целей обработки, при отзыве Пользователем согласия на обработку ПДн, при удалении Учётной записи, при прекращении Соглашения, а также по требованию Пользователя (ст. 21 152-ФЗ), за исключением случаев, когда законодательством РФ предусмотрено обязательное хранение таких данных в течение определённого срока.
10.3. При удалении Учётной записи ПДн Пользователя удаляются в срок не более **[30]** календарных дней с момента получения соответствующего требования, если иное не предусмотрено законодательством. Публичный контент Пользователя (опубликованные Сценарии и т.п.) при этом сохраняется в Сервисе: исключительное право на него отчуждено Оператору (п. 7.2 Пользовательского соглашения, ст. 1234 ГК РФ), а связь контента с персональными данными Пользователя прекращается (обезличивание, ст. 3 152-ФЗ).
## 11. Защита прав несовершеннолетних
11.1. Сервис предназначен для лиц, достигших 18 лет (п. 4.1 Соглашения). Регистрация и использование Сервиса лицами, не достигшими 18 лет, запрещены.
11.2. Оператор не собирает сознательно ПДн лиц, не достигших 18 лет. При обнаружении факта обработки ПДн несовершеннолетнего Оператор незамедлительно прекращает такую обработку и удаляет соответствующие данные.
## 12. Права субъекта ПДн и порядок их реализации
12.1. Пользователь (субъект ПДн) вправе (ст. 14, 17 152-ФЗ):
- получать информацию, касающуюся обработки его ПДн (сведения, предусмотренные ст. 14 152-ФЗ);
- требовать уточнения, блокирования или уничтожения своих ПДн, если они являются неполными, устаревшими, неточными, незаконно полученными или не являются необходимыми для заявленной цели обработки (ст. 14, 21 152-ФЗ);
- отозвать согласие на обработку ПДн в любое время;
- обжаловать действия (бездействие) Оператора в уполномоченном органе по защите прав субъектов персональных данных (Роскомнадзор) и/или в судебном порядке (ст. 17 152-ФЗ).
12.2. Для реализации прав, предусмотренных п. 12.1 Политики, Пользователь направляет Оператору запрос в письменной форме (в т.ч. в форме электронного документа) на адрес: **[email]**.
12.3. Запрос должен содержать (ст. 14 ч. 3, 152-ФЗ):
- номер основного документа, удостоверяющего личность Пользователя (или его представителя), сведения о дате выдачи и выдавшем органе;
- сведения, подтверждающие участие Пользователя в отношениях с Оператором (например, адрес электронной почты, использованный при регистрации);
- подпись Пользователя (или его представителя).
12.4. Оператор рассматривает запрос и направляет ответ в срок, не превышающий **10 рабочих дней** с даты получения запроса (ст. 20 ч. 1 152-ФЗ). В случае отказа в удовлетворении запроса Оператор направляет мотивированный ответ.
12.5. Отзыв согласия на обработку ПДн (ст. 9 ч. 2, 152-ФЗ) влечёт прекращение обработки и уничтожение ПДн в срок, не превышающий **30 календарных дней** с даты получения отзыва (ст. 21 ч. 5, 152-ФЗ). Отзыв согласия может сделать невозможным дальнейшее использование Сервиса.
## 13. Заключительные положения
13.1. Оператор вправе в одностороннем порядке изменять условия Политики. Новая редакция Политики вступает в силу с момента её опубликования в Сервисе, если иной срок не указан при опубликовании. Продолжение использования Сервиса после вступления в силу новой редакции Политики означает согласие Пользователя с изменениями.
13.2. В случае внесения существенных изменений в Политику, затрагивающих права Пользователей, Оператор уведомляет Пользователей способами, доступными в Сервисе (в т.ч. путём направления уведомления на адрес электронной почты).
13.3. Контроль за соблюдением Политики осуществляет лицо, ответственное за организацию обработки ПДн (п. 9.1 Политики).
13.4. По всем вопросам, связанным с обработкой ПДн и реализацией прав субъектов ПДн, Пользователь может обратиться к Оператору по адресу: **[email]**.
13.5. Настоящая Политика регулируется и толкуется в соответствии с законодательством Российской Федерации.
## 14. Реквизиты Оператора
**Оператор Сервиса «Вечерний детектив»:**
- ФИО: **[ФИО Оператора]**
- Адрес: **[адрес]**
- Адрес электронной почты: **[email]**
- Иные реквизиты: **[ИНН (при наличии), иные сведения]**
**Адрес для обращений по вопросам обработки персональных данных: [email].**
+245
View File
@@ -0,0 +1,245 @@
# ПОЛЬЗОВАТЕЛЬСКОЕ СОГЛАШЕНИЕ (ОФЕРТА)
Сервис «Вечерний детектив»
**Редакция от «__» ________ 20__ года**
*Настоящий документ является шаблоном и носит информационный характер. Перед публикацией документ должен быть адаптирован и утверждён квалифицированным юристом.*
---
## 1. Общие положения
1.1. Настоящее Пользовательское соглашение (далее — **Соглашение**) является публичной офертой (ст. 435, 437 Гражданского кодекса РФ, далее — ГК РФ) и определяет условия использования Сервиса «Вечерний детектив» (далее — **Сервис**), размещённого в сети Интернет по адресу: **[адрес сайта]**.
1.2. Оператор Сервиса и правообладатель — **[ФИО Оператора]** (далее — **Оператор**). Реквизиты Оператора указаны в разделе 14 Соглашения.
1.3. Использование Сервиса регулируется:
- настоящим Соглашением;
- Политикой конфиденциальности и обработки персональных данных (далее — **Политика конфиденциальности**), размещённой по адресу: **[адрес политики]**;
- законодательством Российской Федерации.
1.4. Соглашение является договором присоединения (ст. 428 ГК РФ). Акцептом Соглашения признаётся совершение Пользователем любого из следующих действий (ст. 438 ГК РФ, ст. 1286 ГК РФ):
- регистрация учётной записи в Сервисе;
- начало использования Сервиса (в т.ч. вход в Сервис, создание игры, подача заявки на участие, загрузка материалов);
- иное действие, свидетельствующее о принятии условий Соглашения.
1.5. С момента акцепта Соглашение считается заключённым между Пользователем и Оператором в письменной форме (путём обмена документами в электронном виде) на условиях, изложенных в Соглашении, и действует до прекращения в порядке, предусмотренном Соглашением.
1.6. Оператор вправе в одностороннем порядке изменять условия Соглашения. Новая редакция Соглашения вступает в силу с момента её опубликования в Сервисе, если иной срок не указан при опубликовании. Продолжение использования Сервиса после вступления в силу новой редакции означает согласие Пользователя с изменениями.
1.7. Если Пользователь не согласен с условиями Соглашения или его изменениями, он обязан прекратить использование Сервиса.
1.8. Оператор вправе в любое время без уведомления Пользователя вносить изменения в функциональность Сервиса, добавлять или удалять отдельные функции, изменять их содержание.
## 2. Термины
2.1. В целях настоящего Соглашения применяются следующие термины:
- **Пользователь** — дееспособное физическое лицо, достигшее возраста 18 лет, акцептовавшее Соглашение и использующее Сервис;
- **Учётная запись (аккаунт)** — совокупность данных Пользователя, создаваемая при регистрации в Сервисе, позволяющая идентифицировать Пользователя;
- **Игра** — организованное Оператором или Пользователем мероприятие (детективная игра) в рамках Сервиса;
- **Сценарий** — текстовая и/или мультимедийная основа Игры, создаваемая Оператором или Пользователем;
- **Команда** — объединение Пользователей для участия в Игре;
- **Пользовательский контент** — материалы (тексты, изображения, аудио, видео, сценарии, иные объекты), созданные Пользователем и размещённые им в Сервисе;
- **Личный кабинет** — персонализированный раздел Сервиса, доступный Пользователю после авторизации.
2.2. Иные термины, не определённые в Соглашении, используются в значениях, установленных законодательством Российской Федерации и Политикой конфиденциальности.
## 3. Предмет Соглашения
3.1. Оператор предоставляет Пользователю доступ к Сервису на условиях простой (неисключительной) лицензии (ст. 1235, 1236 ГК РФ), а Пользователь обязуется использовать Сервис способами, предусмотренными Соглашением.
3.2. Сервис предоставляет Пользователю возможность:
- регистрироваться и использовать Учётную запись;
- создавать Команды и Игры, подавать заявки на участие в Играх;
- создавать, загружать и использовать Сценарии и иной Пользовательский контент;
- взаимодействовать с другими Пользователями в рамках Сервиса;
- использовать иные функции Сервиса по мере их появления.
3.3. Способы использования Сервиса (ст. 1235 п. 2 ГК РФ): воспроизведение программного обеспечения Сервиса в объёме, необходимом для доступа к Сервису через сеть Интернет, и использование функций Сервиса в соответствии с его назначением. Пользователю не предоставляются права на изменение, распространение, модификацию программного обеспечения Сервиса.
## 4. Условия регистрации и Учётная запись
4.1. Регистрация в Сервисе возможна только для физических лиц, достигших возраста 18 лет. Использование Сервиса лицами, не достигшими 18 лет, запрещено. Регистрируясь, Пользователь подтверждает, что достиг 18 лет и имеет право заключать Соглашение.
4.2. При регистрации Пользователь предоставляет достоверные данные: адрес электронной почты и иные сведения, запрашиваемые Сервисом. Пользователь несёт ответственность за достоверность предоставленных данных.
4.3. Пароль для доступа к Учётной записи направляется Пользователю на адрес электронной почты, указанный при регистрации. Пользователь обязан:
- обеспечить сохранность пароля и не передавать его третьим лицам;
- незамедлительно уведомить Оператора о любом несанкционированном доступе к Учётной записи;
- принимать разумные меры для защиты Учётной записи от несанкционированного доступа.
4.4. Пользователь несёт ответственность за все действия, совершённые с использованием его Учётной записи, если он не докажет, что действия совершены без его участия вследствие обстоятельств непреодолимой силы или вины Оператора.
4.5. Оператор вправе заблокировать или удалить Учётную запись Пользователя (полностью или частично), а также ограничить доступ к Сервису, в случае нарушения Пользователем условий Соглашения, требований законодательства, а также по иным основаниям, предусмотренным Соглашением. Оператор вправе удалить Учётную запись, не использовавшуюся Пользователем в течение **[12]** месяцев.
4.6. Учётная запись не может быть передана или отчуждена третьим лицам, за исключением случаев, предусмотренных законом или прямо разрешённых Оператором.
## 5. Права и обязанности Пользователя
5.1. Пользователь вправе:
- использовать Сервис в соответствии с Соглашением и его назначением;
- создавать Пользовательский контент и размещать его в Сервисе;
- обращаться к Оператору по вопросам функционирования Сервиса;
- реализовывать иные права, предусмотренные Соглашением и законодательством.
5.2. Пользователь обязуется:
- соблюдать условия Соглашения, Политики конфиденциальности и требования законодательства РФ;
- не использовать Сервис для целей, противоречащих законодательству;
- не совершать действий, направленных на нарушение функционирования Сервиса, его программного обеспечения, баз данных;
- не пытаться получить несанкционированный доступ к Сервису, его системам и данным других Пользователей;
- не использовать автоматизированные средства (боты, скрипты, парсеры) для взаимодействия с Сервисом без предварительного письменного разрешения Оператора;
- не размещать в Сервисе материалы, перечисленные в разделе 6 Соглашения;
- не воспроизводить, не копировать, не изменять, не декомпилировать, не дизассемблировать программное обеспечение Сервиса, за исключением случаев, прямо разрешённых законом (ст. 1280 ГК РФ);
- не использовать Сервис для обхода или нарушения прав третьих лиц, включая права на объекты интеллектуальной собственности.
## 6. Запрещённый контент и правила поведения
6.1. В Сервисе запрещено размещение Пользовательского контента, который:
- нарушает законодательство РФ, в т.ч. авторские и смежные права, права на товарные знаки, иные права третьих лиц;
- содержит порнографические материалы, а также материалы сексуального характера с участием несовершеннолетних;
- содержит призывы к насилию, экстремизму, терроризму, разжигание национальной, расовой, религиозной или иной розни;
- содержит клевету, оскорбления, угрозы, преследование или иным образом нарушает права других лиц;
- содержит пропаганду наркотических средств, алкоголя, азартных игр (в запрещённых законом формах);
- содержит вредоносные программы, вирусы, ссылки на них;
- нарушает права несовершеннолетних;
- иным образом противоречит целям Сервиса и принципам добросовестности.
6.2. Оператор вправе, но не обязан, осуществлять модерацию Пользовательского контента. Оператор вправе удалить любой Пользовательский контент, нарушающий условия Соглашения или законодательство, без предварительного уведомления Пользователя, а также заблокировать или удалить Учётную запись нарушителя.
6.3. Пользователь самостоятельно несёт ответственность за размещаемый им Пользовательский контент, включая его законность, достоверность и соответствие правам третьих лиц.
## 7. Пользовательский контент и права на него
7.1. Личные неимущественные права на Пользовательский контент (право авторства, право на имя) сохраняются за автором и не могут быть отчуждены (ст. 1228, 1265 ГК РФ).
7.2. Переводя Сценарий в статус «опубликован» (публикуя Сценарий в Сервисе), Пользователь безвозмездно отчуждает Оператору принадлежащее ему исключительное право на такой Сценарий в полном объёме (ст. 1234 ГК РФ). Настоящий договор об отчуждении исключительного права является **безвозмездным**. Исключительное право переходит к Оператору в момент перевода Сценария в статус «опубликован» (ст. 1234 п. 4 ГК РФ), на весь срок действия исключительного права, на территории всего мира. Ограничение п. 3.1 ст. 1234 ГК РФ не применяется, поскольку Пользователь не является коммерческой организацией.
7.3. Пользователь соглашается с использованием опубликованных Сценариев Оператором без указания имени автора (анонимно) либо под псевдонимом (никнеймом), указанным Пользователем при регистрации (ст. 1265 п. 1 ГК РФ), в т.ч. после удаления Учётной записи Пользователя.
7.4. Сценарии, не переведённые в статус «опубликован» (черновики), не отчуждаются: Пользователь сохраняет исключительное право на такие Сценарии и вправе свободно их редактировать, изменять и удалять.
7.5. Пользователь гарантирует, что:
- он обладает достаточными правами на размещаемый Пользовательский контент (является автором и/или правообладателем либо имеет разрешение правообладателя);
- размещение контента не нарушает прав третьих лиц и законодательства РФ;
- контент не содержит персональных данных третьих лиц без их согласия;
- на момент публикации Сценария он вправе отчуждать исключительное право на него Оператору.
7.6. Право на отзыв (ст. 1269 ГК РФ) действует только **до фактического обнародования** Сценария. После перевода Сценария в статус «опубликован» Пользователь не вправе отозвать Сценарий, требовать прекращения его использования, удаления или снятия с публикации. Оператор вправе использовать опубликованный Сценарий любыми способами, включая воспроизведение, распространение, переработку, включение в составные и сложные объекты, доведение до всеобщего сведения, предоставление доступа третьим лицам, — без дополнительного согласия Пользователя.
7.7. При получении претензий третьих лиц в отношении Пользовательского контента Оператор вправе временно ограничить доступ к такому контенту до урегулирования спора и уведомить об этом Пользователя.
7.8. Удаление Учётной записи Пользователя не прекращает отчуждение исключительного права на опубликованные Сценарии (ст. 1234 п. 1 ГК РФ). При удалении Учётной записи Оператор удаляет персональные данные Пользователя, при этом опубликованные Сценарии сохраняются в Сервисе, а атрибуция автора обезличивается (ст. 3 152-ФЗ, обезличивание) в порядке, предусмотренном п. 7.3 Соглашения.
## 8. Платные услуги
8.1. На дату вступления Соглашения в силу Сервис предоставляется Пользователям бесплатно. Отдельные функции Сервиса могут стать платными в будущем.
8.2. Оператор вправе вводить платные услуги (функции) Сервиса, а также изменять стоимость платных услуг, уведомив Пользователей не менее чем за **[14]** дней путём опубликования соответствующей информации в Сервисе и/или направления уведомления на адрес электронной почты Пользователя.
8.3. При введении платных услуг условия их предоставления, включая порядок оплаты, сроки, порядок возврата денежных средств, определяются отдельными документами (правилами), размещаемыми в Сервисе. Использование платных услуг означает согласие Пользователя с соответствующими правилами.
8.4. Если Сервис будет предоставлять платные услуги, отношения между Оператором и Пользователем в части таких услуг регулируются, в т.ч., Законом РФ от 07.02.1992 № 2300-1 «О защите прав потребителей» (далее — Закон о защите прав потребителей) в части, применимой к отношениям сторон.
## 9. Персональные данные
9.1. При использовании Сервиса Оператор обрабатывает персональные данные Пользователя (ст. 3 Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных», далее — 152-ФЗ): адрес электронной почты, имя (псевдоним), данные Учётной записи, сведения о действиях Пользователя в Сервисе, технические данные (IP-адрес, файлы cookie, данные об устройстве), иные данные, указанные в Политике конфиденциальности.
9.2. Обработка персональных данных осуществляется на основании (ст. 6 152-ФЗ):
- согласия Пользователя, выраженного при регистрации и/или использовании Сервиса (акцепт Политики конфиденциальности);
- необходимости исполнения Соглашения, стороной которого является Пользователь;
- иных законных оснований.
9.3. Цели обработки персональных данных: идентификация Пользователя, предоставление доступа к Сервису, обеспечение функционирования Сервиса, связь с Пользователем (в т.ч. направление пароля и уведомлений), улучшение качества Сервиса, исполнение требований законодательства.
9.4. Оператор обрабатывает персональные данные в соответствии с принципами законности, справедливости, минимизации и ограничения сроков хранения (ст. 5 152-ФЗ), принимает правовые, организационные и технические меры защиты (ст. 19 152-ФЗ).
9.5. Пользователь вправе:
- получать информацию о своих персональных данных и их обработке (ст. 14 152-ФЗ);
- требовать уточнения, блокирования, уничтожения персональных данных (ст. 14, 21 152-ФЗ);
- отозвать согласие на обработку персональных данных в любое время (ст. 9 152-ФЗ), направив соответствующее требование Оператору. Отзыв согласия может повлечь невозможность использования Сервиса;
- обжаловать действия Оператора в уполномоченный орган (Роскомнадзор) и в суд (ст. 17 152-ФЗ).
9.6. Порядок, сроки и условия обработки персональных данных, включая сведения о cookies и аналитике, подробно изложены в Политике конфиденциальности, размещённой по адресу: **[адрес политики]**.
9.7. Оператор не передаёт персональные данные Пользователя третьим лицам, за исключением случаев, предусмотренных законодательством РФ или прямо указанных в Политике конфиденциальности (в т.ч. техническим подрядчикам, обеспечивающим функционирование Сервиса).
9.8. Оператор обеспечивает запись, систематизацию, накопление, хранение, уточнение, извлечение персональных данных граждан Российской Федерации с использованием баз данных, находящихся на территории Российской Федерации (ст. 18 ч. 5 152-ФЗ).
## 10. Интеллектуальная собственность
10.1. Исключительные права на программное обеспечение Сервиса, его дизайн, интерфейс, логотипы, товарные знаки, тексты, базы данных и иные материалы, созданные Оператором (за исключением Пользовательского контента), принадлежат Оператору (ст. 1225, 1229, 1261 ГК РФ).
10.2. Использование материалов, указанных в п. 10.1, допускается только в пределах функций Сервиса. Запрещается копирование, распространение, переработка, использование материалов Сервиса вне Сервиса без письменного согласия Оператора, за исключением случаев, предусмотренных законодательством.
10.3. Пользователь обязуется не нарушать исключительные права Оператора и третьих лиц при использовании Сервиса. При нарушении исключительных прав правообладатель вправе требовать защиты способами, предусмотренными ст. 1252, 1301 ГК РФ.
10.4. Нарушение условий Соглашения о способах использования Сервиса (раздел 3) является нарушением условий лицензии и может являться основанием для её одностороннего прекращения Оператором (ст. 450.1, 1286.1 ГК РФ).
## 11. Ответственность сторон
11.1. За неисполнение или ненадлежащее исполнение обязательств по Соглашению стороны несут ответственность в соответствии с законодательством РФ (ст. 393, 401 ГК РФ).
11.2. Сервис предоставляется «как есть» (as is). Оператор не гарантирует:
- бесперебойную и безошибочную работу Сервиса;
- соответствие Сервиса ожиданиям Пользователя;
- сохранность Пользовательского контента (Пользователю рекомендуется создавать резервные копии значимых материалов);
- отсутствие технических сбоев, в т.ч. связанных с работой сети Интернет, хостинга, оборудования.
11.3. Оператор не несёт ответственности за убытки Пользователя, возникшие в результате:
- невозможности использования Сервиса по причинам, не зависящим от Оператора;
- действий (бездействия) Пользователя, включая передачу пароля третьим лицам и нарушение требований безопасности;
- действий третьих лиц, включая неправомерный доступ к Учётной записи;
- использования Пользователем Сервиса в целях, не предусмотренных Соглашением.
11.4. Размер ответственности Оператора по Соглашению ограничивается суммой **[размер, например: фактически уплаченных Пользователем средств за последние 12 месяцев / 10 000 (десять тысяч) рублей]**, за исключением случаев причинения вреда, за которые ограничение ответственности не допускается законом.
11.5. Оператор не несёт ответственности за действия Пользователей в рамках Игр и за содержание Пользовательского контента.
11.6. В части платных услуг (раздел 8) положения настоящего раздела применяются с учётом императивных требований Закона о защите прав потребителей.
## 12. Изменение, приостановление и прекращение Соглашения
12.1. Оператор вправе приостановить или прекратить предоставление Сервиса (полностью или частично) в любое время, в т.ч. в связи с техническими, юридическими или коммерческими причинами, уведомив Пользователей способами, доступными в Сервисе.
12.2. Пользователь вправе в одностороннем порядке отказаться от Соглашения в любое время, прекратив использование Сервиса и (или) удалив Учётную запись.
12.3. Оператор вправе расторгнуть Соглашение в одностороннем порядке при нарушении Пользователем условий Соглашения, в т.ч. в случаях, указанных в п. 4.5 и п. 6.2 Соглашения, без возмещения каких-либо убытков Пользователю, за исключением случаев, предусмотренных законом.
12.4. При прекращении Соглашения Пользователь утрачивает право использования Сервиса. Положения Соглашения, которые по своему характеру должны продолжать действовать после прекращения (в т.ч. разделы 7, 9, 10, 11, 13), сохраняют силу.
## 13. Разрешение споров
13.1. Все споры и разногласия, возникающие из Соглашения или в связи с ним, стороны разрешают путём переговоров. Претензионный порядок обязателен: срок рассмотрения претензии — **[30]** календарных дней с момента её получения.
13.2. При недостижении согласия спор подлежит рассмотрению в суде по месту нахождения Оператора, за исключением случаев, когда в соответствии с законодательством РФ, в т.ч. Законом о защите прав потребителей, предусмотрена подсудность по выбору Пользователя-потребителя (ст. 17 Закона о защите прав потребителей, ст. 29 ГПК РФ).
13.3. Применимым правом является право Российской Федерации.
## 14. Реквизиты Оператора
**Оператор Сервиса «Вечерний детектив»:**
- ФИО: **[ФИО Оператора]**
- Адрес: **[адрес]**
- Адрес электронной почты: **[email]**
- Иные реквизиты: **[ИНН (при наличии), иные сведения]**
**Адрес для обращений по вопросам, связанным с Соглашением и обработкой персональных данных: [email].**
---
*Пользователь подтверждает, что ознакомился с условиями Соглашения, Политикой конфиденциальности, понимает их значение и согласен с ними.*
+9
View File
@@ -0,0 +1,9 @@
// Package docs встраивает юридические документы в бинарник и раздаёт их
// по постоянным URL (/terms, /privacy, /consent), на которые сервис
// ссылается при фиксации акцептов соглашений (user_agreements).
package docs
import "embed"
//go:embed TERMS.md PRIVACY.md CONSENT.md
var FS embed.FS
+16
View File
@@ -0,0 +1,16 @@
package docs
import "testing"
func TestLegalDocsEmbedded(t *testing.T) {
for _, name := range []string{"TERMS.md", "PRIVACY.md", "CONSENT.md"} {
data, err := FS.ReadFile(name)
if err != nil {
t.Errorf("документ %s не встроен: %v", name, err)
continue
}
if len(data) == 0 {
t.Errorf("документ %s пустой", name)
}
}
}
+96
View File
@@ -0,0 +1,96 @@
package app
import (
"context"
"testing"
"google.golang.org/grpc/metadata"
)
func TestClientInfoFromContext(t *testing.T) {
tests := []struct {
name string
md metadata.MD
wantIP string
wantUserAgent string
}{
{
name: "нет метаданных",
wantIP: "",
wantUserAgent: "",
},
{
name: "x-forwarded-for через gateway: берём последний элемент",
md: metadata.MD{
"x-forwarded-for": []string{"203.0.113.1, 198.51.100.7"},
"grpcgateway-user-agent": []string{"Mozilla/5.0 (test)"},
},
wantIP: "198.51.100.7",
wantUserAgent: "Mozilla/5.0 (test)",
},
{
name: "x-real-ip от доверенного прокси приоритетнее x-forwarded-for",
md: metadata.MD{
"x-real-ip": []string{"192.0.2.10"},
"x-forwarded-for": []string{"203.0.113.1, 10.0.0.1"},
"grpcgateway-user-agent": []string{"curl/8.0"},
},
wantIP: "192.0.2.10",
wantUserAgent: "curl/8.0",
},
{
name: "x-real-ip пустой — фолбэк на x-forwarded-for",
md: metadata.MD{
"x-real-ip": []string{" "},
"x-forwarded-for": []string{"203.0.113.5"},
},
wantIP: "203.0.113.5",
},
{
name: "user-agent для прямого gRPC-клиента",
md: metadata.MD{
"user-agent": []string{"grpc-go/1.60"},
},
wantUserAgent: "grpc-go/1.60",
},
{
name: "x-forwarded-for с пробелами после запятых",
md: metadata.MD{
"x-forwarded-for": []string{"203.0.113.1 , 198.51.100.7 "},
},
wantIP: "198.51.100.7",
},
{
name: "невалидный x-real-ip — фолбэк на валидный x-forwarded-for",
md: metadata.MD{
"x-real-ip": []string{"not-an-ip; drop me"},
"x-forwarded-for": []string{"203.0.113.1, 198.51.100.7"},
},
wantIP: "198.51.100.7",
},
{
name: "все источники невалидны — IP пустой (спуфинг)",
md: metadata.MD{
"x-real-ip": []string{"not-an-ip; drop me"},
"x-forwarded-for": []string{"256.256.256.256"},
},
wantIP: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var ctx context.Context = context.Background()
if tt.md != nil {
ctx = metadata.NewIncomingContext(ctx, tt.md)
}
ip, userAgent := clientInfoFromContext(ctx)
if ip != tt.wantIP {
t.Errorf("ip = %q, want %q", ip, tt.wantIP)
}
if userAgent != tt.wantUserAgent {
t.Errorf("userAgent = %q, want %q", userAgent, tt.wantUserAgent)
}
})
}
}
+65
View File
@@ -0,0 +1,65 @@
package app
import (
"context"
"strings"
"testing"
proto "evening_detective_server/proto"
)
func TestGetLegalDocs(t *testing.T) {
s := &server{}
tests := []struct {
name string
call func() (text, errText string)
wantPart string
}{
{
name: "GetTerms отдаёт Пользовательское соглашение",
call: func() (string, string) {
rsp, err := s.GetTerms(context.Background(), &proto.GetTermsReq{})
if err != nil {
t.Fatalf("GetTerms error: %v", err)
}
return rsp.Text, rsp.Error
},
wantPart: "СОГЛАШЕНИЕ",
},
{
name: "GetPrivacy отдаёт Политику конфиденциальности",
call: func() (string, string) {
rsp, err := s.GetPrivacy(context.Background(), &proto.GetPrivacyReq{})
if err != nil {
t.Fatalf("GetPrivacy error: %v", err)
}
return rsp.Text, rsp.Error
},
wantPart: "КОНФИДЕНЦИАЛЬНОСТИ",
},
{
name: "GetConsent отдаёт текст согласия на обработку ПДн",
call: func() (string, string) {
rsp, err := s.GetConsent(context.Background(), &proto.GetConsentReq{})
if err != nil {
t.Fatalf("GetConsent error: %v", err)
}
return rsp.Text, rsp.Error
},
wantPart: "СОГЛАСИЕ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
text, errText := tt.call()
if errText != "" {
t.Fatalf("ожидался успех, получена ошибка: %s", errText)
}
if !strings.Contains(text, tt.wantPart) {
t.Errorf("текст документа не содержит %q", tt.wantPart)
}
})
}
}
+144 -2
View File
@@ -2,6 +2,10 @@ package app
import (
"context"
"net/netip"
"strings"
"evening_detective_server/docs"
"evening_detective_server/internal/modules/file_storage"
"evening_detective_server/internal/modules/processor_jwt"
"evening_detective_server/internal/modules/roles"
@@ -14,6 +18,7 @@ import (
"google.golang.org/genproto/googleapis/api/httpbody"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
@@ -52,7 +57,16 @@ func (s *server) Echo(_ context.Context, req *proto.EchoReq) (*proto.EchoRsp, er
}
func (s *server) Signup(ctx context.Context, req *proto.SignupReq) (*proto.SignupRsp, error) {
err := s.usersService.AddUser(ctx, req.Username, req.Email)
ip, userAgent := clientInfoFromContext(ctx)
err := s.usersService.AddUser(
ctx,
req.Username,
req.Email,
req.AcceptTerms,
req.AcceptPrivacy,
ip,
userAgent,
)
if err != nil {
return &proto.SignupRsp{
Error: err.Error(),
@@ -61,6 +75,115 @@ func (s *server) Signup(ctx context.Context, req *proto.SignupReq) (*proto.Signu
return &proto.SignupRsp{}, nil
}
func (s *server) DeleteAccount(ctx context.Context, req *proto.DeleteAccountReq) (*proto.DeleteAccountRsp, error) {
claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "claims is not provided")
}
// Пароль приходит HTTP-заголовком X-Password (пробрасывается матчером
// в main.go), чтобы креденшел не попадал в URL и логи. Для прямых
// gRPC-клиентов заголовок передаётся в metadata под тем же ключом.
password := ""
if md, ok := metadata.FromIncomingContext(ctx); ok {
if values := md.Get("x-password"); len(values) > 0 {
password = values[0]
}
}
err := s.usersService.DeleteAccount(ctx, claims.UserID, password)
if err != nil {
return &proto.DeleteAccountRsp{
Error: err.Error(),
}, nil
}
return &proto.DeleteAccountRsp{}, nil
}
// GetTerms возвращает текст Пользовательского соглашения (постоянный URL
// /api/terms, на который ссылается форма регистрации при акцепте).
func (s *server) GetTerms(_ context.Context, _ *proto.GetTermsReq) (*proto.GetTermsRsp, error) {
text, err := docs.FS.ReadFile("TERMS.md")
if err != nil {
return &proto.GetTermsRsp{Error: "Не удалось получить документ"}, nil
}
return &proto.GetTermsRsp{Text: string(text)}, nil
}
// GetPrivacy возвращает текст Политики конфиденциальности (постоянный URL
// /api/privacy).
func (s *server) GetPrivacy(_ context.Context, _ *proto.GetPrivacyReq) (*proto.GetPrivacyRsp, error) {
text, err := docs.FS.ReadFile("PRIVACY.md")
if err != nil {
return &proto.GetPrivacyRsp{Error: "Не удалось получить документ"}, nil
}
return &proto.GetPrivacyRsp{Text: string(text)}, nil
}
// GetConsent возвращает текст согласия на обработку персональных данных
// (постоянный URL /api/consent).
func (s *server) GetConsent(_ context.Context, _ *proto.GetConsentReq) (*proto.GetConsentRsp, error) {
text, err := docs.FS.ReadFile("CONSENT.md")
if err != nil {
return &proto.GetConsentRsp{Error: "Не удалось получить документ"}, nil
}
return &proto.GetConsentRsp{Text: string(text)}, nil
}
// clientInfoFromContext извлекает IP-адрес и user-agent из gRPC-метаданных.
//
// grpc-gateway пробрасывает HTTP-заголовки в метаданные так
// (runtime.annotateContext / runtime.DefaultHeaderMatcher):
// - x-forwarded-for — всегда, при этом gateway дописывает реальный RemoteAddr
// клиента последним элементом списка;
// - постоянные HTTP-заголовки (включая User-Agent) — с префиксом
// grpcgateway-;
// - остальные заголовки по умолчанию не пробрасываются: X-Real-IP и
// X-Password включаются явным матчером в main.go.
//
// Полученный IP валидируется netip.ParseAddr: при прямой экспозиции gateway
// клиент может подделать эти заголовки, поэтому в БД попадает только
// корректный адрес.
func clientInfoFromContext(ctx context.Context) (ip string, userAgent string) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", ""
}
// X-Real-IP от доверенного прокси — приоритетен: в этом случае последним
// элементом X-Forwarded-For является адрес прокси, а не клиента.
if values := md.Get("x-real-ip"); len(values) > 0 {
if v := strings.TrimSpace(values[0]); v != "" {
if _, err := netip.ParseAddr(v); err == nil {
ip = v
}
}
}
if ip == "" {
if values := md.Get("x-forwarded-for"); len(values) > 0 {
parts := strings.Split(values[0], ",")
// Идём с конца списка: последний элемент — реальный RemoteAddr
// клиента (первые элементы мог подделать сам клиент).
for i := len(parts) - 1; i >= 0; i-- {
v := strings.TrimSpace(parts[i])
if v == "" {
continue
}
if _, err := netip.ParseAddr(v); err == nil {
ip = v
break
}
}
}
}
if values := md.Get("grpcgateway-user-agent"); len(values) > 0 {
userAgent = values[0]
} else if values := md.Get("user-agent"); len(values) > 0 {
userAgent = values[0]
}
return ip, userAgent
}
func (s *server) RefreshPassword(ctx context.Context, req *proto.RefreshPasswordReq) (*proto.RefreshPasswordRsp, error) {
err := s.usersService.RefreshPassword(ctx, req.Email)
if err != nil {
@@ -299,6 +422,8 @@ func (s *server) UpdateScenario(ctx context.Context, req *proto.UpdateScenarioRe
req.Name,
req.Description,
req.Image,
claims.UserID,
roles.HasRole(claims, roles.Admin),
)
if err != nil {
return &proto.UpdateScenarioRsp{
@@ -319,6 +444,8 @@ func (s *server) PublicScenario(ctx context.Context, req *proto.PublicScenarioRe
ctx,
int(req.Id),
"public",
claims.UserID,
roles.HasRole(claims, roles.Admin), // публикация = момент отчуждения прав
)
if err != nil {
return &proto.PublicScenarioRsp{
@@ -331,7 +458,10 @@ func (s *server) PublicScenario(ctx context.Context, req *proto.PublicScenarioRe
func (s *server) DraftScenario(ctx context.Context, req *proto.DraftScenarioReq) (*proto.DraftScenarioRsp, error) {
claims := ctx.Value("claims").(*processor_jwt.JWTClaims)
if !roles.HasRole(claims, roles.Author) {
// Снять с публикации опубликованный сценарий может только админ:
// исключительное право отчуждено Оператору в момент публикации
// (п. 7.2 Пользовательского соглашения, ст. 1234, 1269 ГК РФ).
if !roles.HasRole(claims, roles.Admin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
@@ -339,6 +469,8 @@ func (s *server) DraftScenario(ctx context.Context, req *proto.DraftScenarioReq)
ctx,
int(req.Id),
"draft",
claims.UserID,
true,
)
if err != nil {
return &proto.DraftScenarioRsp{
@@ -355,9 +487,13 @@ func (s *server) DeleteScenario(ctx context.Context, req *proto.DeleteScenarioRe
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
// Автор не может удалить опубликованный сценарий (исключительное право
// отчуждено); админ может.
err := s.scenarioService.DeleteScenarioByID(
ctx,
int(req.Id),
claims.UserID,
roles.HasRole(claims, roles.Admin),
)
if err != nil {
return &proto.DeleteScenarioRsp{
@@ -378,6 +514,8 @@ func (s *server) AddScenarioPlace(ctx context.Context, req *proto.AddScenarioPla
ctx,
int(req.Id),
convertPlace(req.Place),
claims.UserID,
roles.HasRole(claims, roles.Admin),
)
if err != nil {
return &proto.AddScenarioPlaceRsp{
@@ -399,6 +537,8 @@ func (s *server) UpdateScenarioPlace(ctx context.Context, req *proto.UpdateScena
int(req.Id),
req.Code,
convertPlace(req.Place),
claims.UserID,
roles.HasRole(claims, roles.Admin),
)
if err != nil {
return &proto.UpdateScenarioPlaceRsp{
@@ -419,6 +559,8 @@ func (s *server) DeleteScenarioPlace(ctx context.Context, req *proto.DeleteScena
ctx,
int(req.Id),
req.Code,
claims.UserID,
roles.HasRole(claims, roles.Admin),
)
if err != nil {
return &proto.DeleteScenarioPlaceRsp{
+6
View File
@@ -7,7 +7,13 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
// mapUser преобразует пользователя в proto-структуру. Возвращает nil для
// обезличенного автора (аккаунт удалён, контент сохранён) — клиент отображает
// такого автора как «Автор удалён».
func mapUser(o *repos.User) *proto.User {
if o == nil {
return nil
}
return &proto.User{
Id: int32(o.ID),
Username: o.Username,
+37
View File
@@ -0,0 +1,37 @@
package app
import (
"testing"
"time"
"evening_detective_server/internal/repos"
)
func TestMapUser(t *testing.T) {
t.Run("nil — обезличенный автор, возвращает nil", func(t *testing.T) {
if got := mapUser(nil); got != nil {
t.Errorf("mapUser(nil) = %v, want nil", got)
}
})
t.Run("обычный пользователь маппится полностью", func(t *testing.T) {
u := &repos.User{
ID: 42,
Username: "detective",
Email: "d@example.com",
Roles: []string{"user"},
IsActive: true,
CreatedAt: time.Date(2026, 8, 18, 12, 0, 0, 0, time.UTC),
}
got := mapUser(u)
if got == nil {
t.Fatal("mapUser вернул nil для не-nil пользователя")
}
if got.Id != 42 || got.Username != "detective" || got.Email != "d@example.com" {
t.Errorf("неожиданный маппинг: %+v", got)
}
if len(got.Roles) != 1 || got.Roles[0] != "user" {
t.Errorf("roles = %v, want [user]", got.Roles)
}
})
}
+54 -30
View File
@@ -75,17 +75,17 @@ func (r *ScenariosRepo) GetScenariosByAuthorID(
var scenarios []*repos.Scenario
for rows.Next() {
scenario := &repos.Scenario{
Author: &repos.User{},
}
scenario := &repos.Scenario{}
var authorID *int
var authorUsername *string
err := rows.Scan(
&scenario.ID,
&scenario.Name,
&scenario.Description,
&scenario.Image,
&scenario.Scenario,
&scenario.Author.ID,
&scenario.Author.Username,
&authorID,
&authorUsername,
&scenario.Status,
&scenario.UpdatedAt,
&scenario.CreatedAt,
@@ -94,6 +94,9 @@ func (r *ScenariosRepo) GetScenariosByAuthorID(
if err != nil {
return nil, err
}
if authorID != nil && authorUsername != nil {
scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername}
}
scenarios = append(scenarios, scenario)
}
if err := rows.Err(); err != nil {
@@ -134,17 +137,17 @@ func (r *ScenariosRepo) GetScenariosByStatus(
var scenarios []*repos.Scenario
for rows.Next() {
scenario := &repos.Scenario{
Author: &repos.User{},
}
scenario := &repos.Scenario{}
var authorID *int
var authorUsername *string
err := rows.Scan(
&scenario.ID,
&scenario.Name,
&scenario.Description,
&scenario.Image,
&scenario.Scenario,
&scenario.Author.ID,
&scenario.Author.Username,
&authorID,
&authorUsername,
&scenario.Status,
&scenario.UpdatedAt,
&scenario.CreatedAt,
@@ -153,6 +156,9 @@ func (r *ScenariosRepo) GetScenariosByStatus(
if err != nil {
return nil, err
}
if authorID != nil && authorUsername != nil {
scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername}
}
scenarios = append(scenarios, scenario)
}
if err := rows.Err(); err != nil {
@@ -166,9 +172,9 @@ func (r *ScenariosRepo) GetScenarioByID(
ctx context.Context,
id int,
) (*repos.Scenario, error) {
scenario := &repos.Scenario{
Author: &repos.User{},
}
scenario := &repos.Scenario{}
var authorID *int
var authorUsername *string
row := r.pool.QueryRow(
ctx,
`SELECT
@@ -195,8 +201,8 @@ func (r *ScenariosRepo) GetScenarioByID(
&scenario.Description,
&scenario.Image,
&scenario.Scenario,
&scenario.Author.ID,
&scenario.Author.Username,
&authorID,
&authorUsername,
&scenario.Status,
&scenario.UpdatedAt,
&scenario.CreatedAt,
@@ -210,9 +216,15 @@ func (r *ScenariosRepo) GetScenarioByID(
return nil, err
}
// Автор может отсутствовать (обезличен после удаления аккаунта).
if authorID != nil && authorUsername != nil {
scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername}
}
return scenario, nil
}
// UpdateScenarioByID меняет основные поля сценария.
func (r *ScenariosRepo) UpdateScenarioByID(
ctx context.Context,
id int,
@@ -243,50 +255,61 @@ func (r *ScenariosRepo) UpdateScenarioByID(
return nil
}
// UpdateScenarioStatusByID меняет статус сценария. allowPublished=true
// разрешает менять статус опубликованного сценария (только для админа).
// Возвращает false, если строка не обновлена (например, не-админ пытается
// снять с публикации опубликованный сценарий). published_at фиксируется
// только при ПЕРВОЙ публикации: повторная публикация не двигает «момент
// отчуждения» исключительного права (п. 7.2 Пользовательского соглашения).
func (r *ScenariosRepo) UpdateScenarioStatusByID(
ctx context.Context,
id int,
status string,
) error {
allowPublished bool,
) (bool, error) {
tag, err := r.pool.Exec(
ctx,
`UPDATE scenarios
SET
status = $1,
published_at = NOW(),
published_at = CASE
WHEN $1 = 'public' AND published_at IS NULL THEN NOW()
ELSE published_at
END,
updated_at = NOW()
WHERE id = $2`,
WHERE id = $2 AND ($3 OR status <> 'public')`,
status,
id,
allowPublished,
)
if err != nil {
return err
return false, err
}
if tag.RowsAffected() == 0 {
return ErrScenarioNotFound
}
return nil
return tag.RowsAffected() > 0, nil
}
// DeleteScenarioByID помечает сценарий удалённым (soft delete).
// allowPublished=true разрешает удаление опубликованного сценария
// (только для админа). Возвращает false, если строка не обновлена
// (например, не-админ пытается удалить опубликованный сценарий).
func (r *ScenariosRepo) DeleteScenarioByID(
ctx context.Context,
id int,
) error {
allowPublished bool,
) (bool, error) {
tag, err := r.pool.Exec(
ctx,
`UPDATE scenarios
SET
is_deleted = TRUE
WHERE id = $1`,
WHERE id = $1 AND ($2 OR status <> 'public')`,
id,
allowPublished,
)
if err != nil {
return err
return false, err
}
if tag.RowsAffected() == 0 {
return ErrScenarioNotFound
}
return nil
return tag.RowsAffected() > 0, nil
}
func (r *ScenariosRepo) GetStoryByScenarioID(
@@ -315,6 +338,7 @@ func (r *ScenariosRepo) GetStoryByScenarioID(
return story, nil
}
// UpdateStoryByScenarioID обновляет содержимое (историю) сценария.
func (r *ScenariosRepo) UpdateStoryByScenarioID(
ctx context.Context,
id int,
@@ -0,0 +1,18 @@
package user_agreements_repo
// AgreementType — типы документов, акцептуемых пользователем.
const (
AgreementTypeTerms = "terms" // Пользовательское соглашение
AgreementTypePrivacy = "privacy" // Политика конфиденциальности
)
// Agreement — фиксируемый факт акцепта документа (ст. 9 152-ФЗ — согласие в
// форме, позволяющей подтвердить факт его получения). Version должна
// соответствовать опубликованной редакции документа; при изменении текста
// документа версия обязана увеличиваться.
type Agreement struct {
AgreementType string
Version string
IP string
UserAgent string
}
+116 -9
View File
@@ -4,9 +4,11 @@ import (
"context"
"errors"
"evening_detective_server/internal/repos"
"evening_detective_server/internal/repos/user_agreements_repo"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -24,21 +26,40 @@ func NewUserRepo(pool *pgxpool.Pool) *UsersRepo {
}
}
func (s *UsersRepo) AddUser(
// AddUserWithAgreements создаёт пользователя и фиксирует акцепты документов
// (user_agreements) в одной транзакции: пользователь либо существует вместе с
// записями о согласиях, либо не существует вовсе (доказуемое согласие,
// ст. 9 152-ФЗ). Возвращает id созданного пользователя.
func (s *UsersRepo) AddUserWithAgreements(
ctx context.Context,
username string,
email string,
passwordHash string,
roles []string,
) error {
tag, err := s.pool.Exec(
agreements []user_agreements_repo.Agreement,
) (int, error) {
// Инвариант «пользователь не существует без записей о согласиях»
// держится на том, что акцепты передаются всегда.
if len(agreements) == 0 {
return 0, errors.New("Не переданы записи об акцепте соглашений")
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return 0, err
}
defer tx.Rollback(ctx) //nolint:errcheck
var id int
err = tx.QueryRow(
ctx,
`INSERT INTO users (username, email, password_hash, roles)
SELECT $1, $2, $3, $4
WHERE NOT EXISTS (
SELECT 1 FROM users
WHERE username = $5 OR email = $6
)`,
)
RETURNING id`,
username,
email,
passwordHash,
@@ -46,14 +67,41 @@ func (s *UsersRepo) AddUser(
username,
email,
)
).Scan(&id)
if err != nil {
return err
// Ничего не вставлено — занят username или email.
if errors.Is(err, pgx.ErrNoRows) {
return 0, errors.New("Username или email уже используется")
}
// Гонка: между проверкой NOT EXISTS и INSERT другой запрос успел
// создать пользователя с тем же username/email — нарушение
// уникальности. Возвращаем тот же понятный клиенту ответ.
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" { // unique_violation
return 0, errors.New("Username или email уже используется")
}
return 0, err
}
if tag.RowsAffected() == 0 {
return errors.New("username или email уже используется")
for _, agreement := range agreements {
if _, err = tx.Exec(
ctx,
`INSERT INTO user_agreements (user_id, agreement_type, version, ip, user_agent)
VALUES ($1, $2, $3, $4, $5)`,
id,
agreement.AgreementType,
agreement.Version,
agreement.IP,
agreement.UserAgent,
); err != nil {
return 0, err
}
}
return nil
if err = tx.Commit(ctx); err != nil {
return 0, err
}
return id, nil
}
func (s *UsersRepo) UpdateUserPassword(
@@ -207,3 +255,62 @@ func (s *UsersRepo) DeleteUserRole(
)
return err
}
// DeleteUser удаляет учётную запись и персональные данные пользователя
// (ст. 21 152-ФЗ — уничтожение ПДн при удалении аккаунта). Выполняется в одной
// транзакции. Публичный контент пользователя (сценарии, команды) сохраняется:
// FK-ограничения с ON DELETE SET NULL (миграция
// 20260818232725_keep_content_on_user_delete.sql) обнуляют привязку к автору —
// контент остаётся в обезличенном виде.
func (s *UsersRepo) DeleteUser(
ctx context.Context,
userId int,
) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx) //nolint:errcheck
// Refresh-токены (FK CASCADE, удаляем явно для предсказуемости).
if _, err = tx.Exec(
ctx,
`DELETE FROM refresh_tokens WHERE user_id = $1`,
userId,
); err != nil {
return err
}
// Зафиксированные акцепты соглашений НЕ удаляются: ст. 9 ч. 4 152-ФЗ
// возлагает на оператора бремя доказывания получения согласия, поэтому
// факт акцепта (документ, версия, время) сохраняется в обезличенном виде:
// ссылка на пользователя и персональные данные внутри записи (ip,
// user_agent) обнуляются. FK ON DELETE SET NULL (миграция
// 20260818231943_create_user_agreements_table.sql) гарантирует, что при
// удалении users строки акцептов остаются с user_id = NULL.
if _, err = tx.Exec(
ctx,
`UPDATE user_agreements
SET user_id = NULL, ip = NULL, user_agent = NULL
WHERE user_id = $1`,
userId,
); err != nil {
return err
}
// Пользователь. Связанные сценарии (author_id) и команды (creator_id)
// обнуляются автоматически через ON DELETE SET NULL.
tag, err := tx.Exec(
ctx,
`DELETE FROM users WHERE id = $1`,
userId,
)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrUserNotFound
}
return tx.Commit(ctx)
}
+94 -2
View File
@@ -5,6 +5,7 @@ import (
"errors"
"evening_detective_server/internal/modules/cleaner"
"evening_detective_server/internal/modules/storytelling"
"evening_detective_server/internal/repos"
"evening_detective_server/internal/repos/scenarios_repo"
"strings"
)
@@ -88,30 +89,111 @@ func (s *ScenarioService) UpdateScenarioByID(
name string,
description string,
image string,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
return s.scenariosRepo.UpdateScenarioByID(ctx, id, name, description, image)
}
// ErrScenarioPublished — опубликованный сценарий нельзя снять с публикации
// или удалить: при публикации исключительное право отчуждается Оператору
// (п. 7.2 Пользовательского соглашения, ст. 1234 ГК РФ), а право на отзыв
// действует только до обнародования (ст. 1269 ГК РФ).
var ErrScenarioPublished = errors.New("Опубликованный сценарий нельзя снять с публикации или удалить: исключительное право отчуждено Оператору")
// ErrScenarioNotOwner — сценарий принадлежит другому пользователю.
var ErrScenarioNotOwner = errors.New("Сценарий принадлежит другому пользователю")
// getScenarioForChange проверяет права актора на изменение сценария:
// не-админ должен быть автором сценария, сценарий не должен быть удалён.
// Возвращает сценарий, чтобы вызывающий код не делал повторное чтение.
func (s *ScenarioService) getScenarioForChange(
ctx context.Context,
id int,
actorId int,
isAdmin bool,
) (*repos.Scenario, error) {
scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id)
if err != nil {
return nil, err
}
if scenario.IsDeleted {
return nil, scenarios_repo.ErrScenarioNotFound
}
if isAdmin {
return scenario, nil
}
if scenario.Author == nil || scenario.Author.ID != actorId {
return nil, ErrScenarioNotOwner
}
return scenario, nil
}
func (s *ScenarioService) UpdateScenarioStatusByID(
ctx context.Context,
id int,
status string,
actorId int,
isAdmin bool,
) error {
return s.scenariosRepo.UpdateScenarioStatusByID(ctx, id, status)
scenario, err := s.getScenarioForChange(ctx, id, actorId, isAdmin)
if err != nil {
return err
}
if !isAdmin && scenario.Status == "public" {
// Повторная публикация уже опубликованного — идемпотентный успех
// (published_at не двигается: CASE WHEN published_at IS NULL).
if status == "public" {
return nil
}
return ErrScenarioPublished
}
// Guard в SQL защищает от гонки «чтение статуса → запись»: не-админ не
// сможет перевести в draft сценарий, опубликованный между чтением и UPDATE.
updated, err := s.scenariosRepo.UpdateScenarioStatusByID(ctx, id, status, isAdmin)
if err != nil {
return err
}
if !updated {
return ErrScenarioPublished
}
return nil
}
func (s *ScenarioService) DeleteScenarioByID(
ctx context.Context,
id int,
actorId int,
isAdmin bool,
) error {
return s.scenariosRepo.DeleteScenarioByID(ctx, id)
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
// Guard в SQL защищает от гонки «чтение статуса → запись»: не-админ не
// сможет удалить сценарий, опубликованный между чтением и UPDATE.
updated, err := s.scenariosRepo.DeleteScenarioByID(ctx, id, isAdmin)
if err != nil {
return err
}
if !updated {
return ErrScenarioPublished
}
return nil
}
func (s *ScenarioService) AddScenarioPlace(
ctx context.Context,
id int,
place *storytelling.Place,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
@@ -128,7 +210,12 @@ func (s *ScenarioService) UpdateScenarioPlace(
id int,
code string,
place *storytelling.Place,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
@@ -146,7 +233,12 @@ func (s *ScenarioService) DeleteScenarioPlace(
ctx context.Context,
id int,
code string,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
+58 -1
View File
@@ -9,6 +9,7 @@ import (
"evening_detective_server/internal/modules/roles"
"evening_detective_server/internal/repos"
"evening_detective_server/internal/repos/refresh_tokens_repo"
"evening_detective_server/internal/repos/user_agreements_repo"
"evening_detective_server/internal/repos/users_repo"
"fmt"
"slices"
@@ -17,6 +18,17 @@ import (
"golang.org/x/crypto/bcrypt"
)
// Версии документов, акцепт которых фиксируется. Должны совпадать с
// редакциями, опубликованными по постоянным URL (/api/terms и /api/privacy,
// rpc GetTerms/GetPrivacy, текст встраивается из пакета docs): при изменении
// текста документа версия ОБЯЗАНА увеличиваться, иначе повторный акцепт
// не будет зафиксирован как согласие с новой редакцией
// (UNIQUE (user_id, agreement_type, version)).
const (
termsVersion = "1.0"
privacyVersion = "1.0"
)
type UsersService struct {
usersRepo *users_repo.UsersRepo
passwordGenerator password_generator.IPasswordGenerator
@@ -41,11 +53,22 @@ func NewUsersService(
}
}
// ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений.
var ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности")
func (s *UsersService) AddUser(
ctx context.Context,
username string,
email string,
acceptTerms bool,
acceptPrivacy bool,
ip string,
userAgent string,
) error {
if !acceptTerms || !acceptPrivacy {
return ErrTermsNotAccepted
}
password, err := s.passwordGenerator.Generate()
if err != nil {
return err
@@ -54,12 +77,28 @@ func (s *UsersService) AddUser(
if err != nil {
return err
}
err = s.usersRepo.AddUser(
// Создание пользователя и фиксация акцептов — в одной транзакции:
// без записей о согласиях пользователь не существует.
_, err = s.usersRepo.AddUserWithAgreements(
ctx,
username,
email,
string(hashedPassword),
[]string{"user"},
[]user_agreements_repo.Agreement{
{
AgreementType: user_agreements_repo.AgreementTypeTerms,
Version: termsVersion,
IP: ip,
UserAgent: userAgent,
},
{
AgreementType: user_agreements_repo.AgreementTypePrivacy,
Version: privacyVersion,
IP: ip,
UserAgent: userAgent,
},
},
)
if err != nil {
return err
@@ -74,6 +113,24 @@ func (s *UsersService) AddUser(
return err
}
// DeleteAccount удаляет учётную запись и все персональные данные пользователя
// (ст. 21 152-ФЗ). Требует подтверждения паролем: обладатель украденного
// access-токена не должен мочь удалить чужой аккаунт.
func (s *UsersService) DeleteAccount(
ctx context.Context,
userId int,
password string,
) error {
user, err := s.usersRepo.GetUserByID(ctx, userId)
if err != nil {
return err
}
if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return errors.New("Неверный пароль")
}
return s.usersRepo.DeleteUser(ctx, userId)
}
func (s *UsersService) RefreshPassword(
ctx context.Context,
email string,
@@ -0,0 +1,26 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS user_agreements (
id SERIAL PRIMARY KEY,
-- user_id NULL-able: при удалении аккаунта FK-ограничение обнуляет
-- ссылку (ON DELETE SET NULL), а сама запись об акцепте остаётся в
-- обезличенном виде (факт согласия, ст. 9 ч. 4 152-ФЗ — бремя
-- доказывания на операторе). Персональные данные внутри записи
-- (ip, user_agent) обнуляет users_repo.DeleteUser.
user_id INTEGER,
agreement_type VARCHAR(50) NOT NULL,
version VARCHAR(50) NOT NULL,
accepted_at TIMESTAMP NOT NULL DEFAULT NOW (),
ip VARCHAR(45),
user_agent TEXT,
CONSTRAINT fk_user_agreements_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL,
CONSTRAINT uq_user_agreement UNIQUE (user_id, agreement_type, version),
CONSTRAINT chk_user_agreement_type CHECK (agreement_type IN ('terms', 'privacy'))
);
-- Отдельный индекс по user_id не нужен: UNIQUE (user_id, agreement_type, version)
-- уже покрывает выборки по префиксу user_id.
-- +goose Down
DROP TABLE IF EXISTS user_agreements;
@@ -0,0 +1,39 @@
-- +goose Up
-- Сценарии и команды сохраняются при удалении аккаунта автора:
-- привязка к пользователю обнуляется (обезличивание, ст. 3 152-ФЗ),
-- сам контент остаётся в Сервисе (лицензия по п. 7.2 Пользовательского соглашения).
ALTER TABLE scenarios ALTER COLUMN author_id DROP NOT NULL;
ALTER TABLE scenarios DROP CONSTRAINT IF EXISTS fk_scenarios_author;
ALTER TABLE scenarios ADD CONSTRAINT fk_scenarios_author
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE SET NULL;
ALTER TABLE teams ALTER COLUMN creator_id DROP NOT NULL;
ALTER TABLE teams DROP CONSTRAINT IF EXISTS fk_teams_creator;
ALTER TABLE teams ADD CONSTRAINT fk_teams_creator
FOREIGN KEY (creator_id) REFERENCES users (id) ON DELETE SET NULL;
-- status у сценариев всегда задан (default 'draft'): NULL-значение сломало бы
-- guard'ы вида "status <> 'public'" в scenarios_repo (NULL даёт NULL, т.е. ложь).
-- На БД с историческими NULL-данными сначала приводим их к default'у.
UPDATE scenarios SET status = 'draft' WHERE status IS NULL;
ALTER TABLE scenarios ALTER COLUMN status SET NOT NULL;
-- +goose Down
-- Восстановление прежней схемы: автор обязателен, при удалении пользователя —
-- удаление его контента запрещено (RESTRICT).
DELETE FROM scenarios WHERE author_id IS NULL;
DELETE FROM teams WHERE creator_id IS NULL;
ALTER TABLE scenarios ALTER COLUMN status DROP NOT NULL;
ALTER TABLE scenarios DROP CONSTRAINT IF EXISTS fk_scenarios_author;
ALTER TABLE scenarios ADD CONSTRAINT fk_scenarios_author
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE RESTRICT;
ALTER TABLE scenarios ALTER COLUMN author_id SET NOT NULL;
ALTER TABLE teams DROP CONSTRAINT IF EXISTS fk_teams_creator;
ALTER TABLE teams ADD CONSTRAINT fk_teams_creator
FOREIGN KEY (creator_id) REFERENCES users (id) ON DELETE RESTRICT;
ALTER TABLE teams ALTER COLUMN creator_id SET NOT NULL;
+863 -453
View File
File diff suppressed because it is too large Load Diff
+240
View File
@@ -199,6 +199,90 @@ func local_request_EveningDetectiveServer_Refresh_0(ctx context.Context, marshal
return msg, metadata, err
}
func request_EveningDetectiveServer_DeleteAccount_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteAccountReq
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.DeleteAccount(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_DeleteAccount_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteAccountReq
metadata runtime.ServerMetadata
)
msg, err := server.DeleteAccount(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_GetTerms_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetTermsReq
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.GetTerms(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_GetTerms_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetTermsReq
metadata runtime.ServerMetadata
)
msg, err := server.GetTerms(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_GetPrivacy_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetPrivacyReq
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.GetPrivacy(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_GetPrivacy_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetPrivacyReq
metadata runtime.ServerMetadata
)
msg, err := server.GetPrivacy(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_GetConsent_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetConsentReq
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.GetConsent(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_GetConsent_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetConsentReq
metadata runtime.ServerMetadata
)
msg, err := server.GetConsent(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_GetUsers_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetUsersReq
@@ -1754,6 +1838,86 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti
}
forward_EveningDetectiveServer_Refresh_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_EveningDetectiveServer_DeleteAccount_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeleteAccount", runtime.WithHTTPPathPattern("/api/auth/delete-account"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_DeleteAccount_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_DeleteAccount_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTerms_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetTerms", runtime.WithHTTPPathPattern("/api/terms"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_GetTerms_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetTerms_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetPrivacy_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetPrivacy", runtime.WithHTTPPathPattern("/api/privacy"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_GetPrivacy_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetPrivacy_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetConsent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetConsent", runtime.WithHTTPPathPattern("/api/consent"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_GetConsent_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetConsent_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -2636,6 +2800,74 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti
}
forward_EveningDetectiveServer_Refresh_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_EveningDetectiveServer_DeleteAccount_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeleteAccount", runtime.WithHTTPPathPattern("/api/auth/delete-account"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_DeleteAccount_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_DeleteAccount_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTerms_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetTerms", runtime.WithHTTPPathPattern("/api/terms"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_GetTerms_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetTerms_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetPrivacy_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetPrivacy", runtime.WithHTTPPathPattern("/api/privacy"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_GetPrivacy_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetPrivacy_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetConsent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetConsent", runtime.WithHTTPPathPattern("/api/consent"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_GetConsent_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetConsent_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -3275,6 +3507,10 @@ var (
pattern_EveningDetectiveServer_RefreshPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "refresh-password"}, ""))
pattern_EveningDetectiveServer_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "login"}, ""))
pattern_EveningDetectiveServer_Refresh_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "refresh"}, ""))
pattern_EveningDetectiveServer_DeleteAccount_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "delete-account"}, ""))
pattern_EveningDetectiveServer_GetTerms_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "terms"}, ""))
pattern_EveningDetectiveServer_GetPrivacy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "privacy"}, ""))
pattern_EveningDetectiveServer_GetConsent_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "consent"}, ""))
pattern_EveningDetectiveServer_GetUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "users"}, ""))
pattern_EveningDetectiveServer_GetUserById_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "users", "id"}, ""))
pattern_EveningDetectiveServer_GetMe_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "users", "me"}, ""))
@@ -3321,6 +3557,10 @@ var (
forward_EveningDetectiveServer_RefreshPassword_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_Login_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_Refresh_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_DeleteAccount_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetTerms_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetPrivacy_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetConsent_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetUsers_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetUserById_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetMe_0 = runtime.ForwardResponseMessage
+152
View File
@@ -26,6 +26,10 @@ const (
EveningDetectiveServer_RefreshPassword_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/RefreshPassword"
EveningDetectiveServer_Login_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/Login"
EveningDetectiveServer_Refresh_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/Refresh"
EveningDetectiveServer_DeleteAccount_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DeleteAccount"
EveningDetectiveServer_GetTerms_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTerms"
EveningDetectiveServer_GetPrivacy_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetPrivacy"
EveningDetectiveServer_GetConsent_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetConsent"
EveningDetectiveServer_GetUsers_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetUsers"
EveningDetectiveServer_GetUserById_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetUserById"
EveningDetectiveServer_GetMe_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetMe"
@@ -75,6 +79,10 @@ type EveningDetectiveServerClient interface {
RefreshPassword(ctx context.Context, in *RefreshPasswordReq, opts ...grpc.CallOption) (*RefreshPasswordRsp, error)
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginRsp, error)
Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*RefreshRsp, error)
DeleteAccount(ctx context.Context, in *DeleteAccountReq, opts ...grpc.CallOption) (*DeleteAccountRsp, error)
GetTerms(ctx context.Context, in *GetTermsReq, opts ...grpc.CallOption) (*GetTermsRsp, error)
GetPrivacy(ctx context.Context, in *GetPrivacyReq, opts ...grpc.CallOption) (*GetPrivacyRsp, error)
GetConsent(ctx context.Context, in *GetConsentReq, opts ...grpc.CallOption) (*GetConsentRsp, error)
GetUsers(ctx context.Context, in *GetUsersReq, opts ...grpc.CallOption) (*GetUsersRsp, error)
GetUserById(ctx context.Context, in *GetUserByIdReq, opts ...grpc.CallOption) (*GetUserByIdRsp, error)
GetMe(ctx context.Context, in *GetMeReq, opts ...grpc.CallOption) (*GetMeRsp, error)
@@ -182,6 +190,46 @@ func (c *eveningDetectiveServerClient) Refresh(ctx context.Context, in *RefreshR
return out, nil
}
func (c *eveningDetectiveServerClient) DeleteAccount(ctx context.Context, in *DeleteAccountReq, opts ...grpc.CallOption) (*DeleteAccountRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteAccountRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_DeleteAccount_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) GetTerms(ctx context.Context, in *GetTermsReq, opts ...grpc.CallOption) (*GetTermsRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetTermsRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_GetTerms_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) GetPrivacy(ctx context.Context, in *GetPrivacyReq, opts ...grpc.CallOption) (*GetPrivacyRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetPrivacyRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_GetPrivacy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) GetConsent(ctx context.Context, in *GetConsentReq, opts ...grpc.CallOption) (*GetConsentRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetConsentRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_GetConsent_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) GetUsers(ctx context.Context, in *GetUsersReq, opts ...grpc.CallOption) (*GetUsersRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetUsersRsp)
@@ -562,6 +610,10 @@ type EveningDetectiveServerServer interface {
RefreshPassword(context.Context, *RefreshPasswordReq) (*RefreshPasswordRsp, error)
Login(context.Context, *LoginReq) (*LoginRsp, error)
Refresh(context.Context, *RefreshReq) (*RefreshRsp, error)
DeleteAccount(context.Context, *DeleteAccountReq) (*DeleteAccountRsp, error)
GetTerms(context.Context, *GetTermsReq) (*GetTermsRsp, error)
GetPrivacy(context.Context, *GetPrivacyReq) (*GetPrivacyRsp, error)
GetConsent(context.Context, *GetConsentReq) (*GetConsentRsp, error)
GetUsers(context.Context, *GetUsersReq) (*GetUsersRsp, error)
GetUserById(context.Context, *GetUserByIdReq) (*GetUserByIdRsp, error)
GetMe(context.Context, *GetMeReq) (*GetMeRsp, error)
@@ -627,6 +679,18 @@ func (UnimplementedEveningDetectiveServerServer) Login(context.Context, *LoginRe
func (UnimplementedEveningDetectiveServerServer) Refresh(context.Context, *RefreshReq) (*RefreshRsp, error) {
return nil, status.Error(codes.Unimplemented, "method Refresh not implemented")
}
func (UnimplementedEveningDetectiveServerServer) DeleteAccount(context.Context, *DeleteAccountReq) (*DeleteAccountRsp, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteAccount not implemented")
}
func (UnimplementedEveningDetectiveServerServer) GetTerms(context.Context, *GetTermsReq) (*GetTermsRsp, error) {
return nil, status.Error(codes.Unimplemented, "method GetTerms not implemented")
}
func (UnimplementedEveningDetectiveServerServer) GetPrivacy(context.Context, *GetPrivacyReq) (*GetPrivacyRsp, error) {
return nil, status.Error(codes.Unimplemented, "method GetPrivacy not implemented")
}
func (UnimplementedEveningDetectiveServerServer) GetConsent(context.Context, *GetConsentReq) (*GetConsentRsp, error) {
return nil, status.Error(codes.Unimplemented, "method GetConsent not implemented")
}
func (UnimplementedEveningDetectiveServerServer) GetUsers(context.Context, *GetUsersReq) (*GetUsersRsp, error) {
return nil, status.Error(codes.Unimplemented, "method GetUsers not implemented")
}
@@ -868,6 +932,78 @@ func _EveningDetectiveServer_Refresh_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_DeleteAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteAccountReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).DeleteAccount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_DeleteAccount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).DeleteAccount(ctx, req.(*DeleteAccountReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_GetTerms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetTermsReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).GetTerms(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_GetTerms_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).GetTerms(ctx, req.(*GetTermsReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_GetPrivacy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetPrivacyReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).GetPrivacy(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_GetPrivacy_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).GetPrivacy(ctx, req.(*GetPrivacyReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_GetConsent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetConsentReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).GetConsent(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_GetConsent_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).GetConsent(ctx, req.(*GetConsentReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_GetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUsersReq)
if err := dec(in); err != nil {
@@ -1565,6 +1701,22 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
MethodName: "Refresh",
Handler: _EveningDetectiveServer_Refresh_Handler,
},
{
MethodName: "DeleteAccount",
Handler: _EveningDetectiveServer_DeleteAccount_Handler,
},
{
MethodName: "GetTerms",
Handler: _EveningDetectiveServer_GetTerms_Handler,
},
{
MethodName: "GetPrivacy",
Handler: _EveningDetectiveServer_GetPrivacy_Handler,
},
{
MethodName: "GetConsent",
Handler: _EveningDetectiveServer_GetConsent_Handler,
},
{
MethodName: "GetUsers",
Handler: _EveningDetectiveServer_GetUsers_Handler,