generated from VLADIMIR/template
Compare commits
3 Commits
72605a3cd4
...
a797893a29
| Author | SHA1 | Date | |
|---|---|---|---|
| a797893a29 | |||
| 268b359576 | |||
| 204a20c814 |
@@ -39,6 +39,18 @@ goose -dir ./migrations postgres "user=postgres password=postgres host=localhost
|
|||||||
make test
|
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)
|
## Агент «Юрист по ПО» (DSH)
|
||||||
|
|
||||||
В каталоге [`agents/software-lawyer`](agents/software-lawyer) лежит агент-пресет для DeepSeek Harness: специализированный консультант по правовым вопросам разработки ПО (лицензии, авторские права, персональные данные 152-ФЗ/GDPR, договоры). Отвечает на основе встроенной базы знаний (`skills/software-law/references/`), без внешних API.
|
В каталоге [`agents/software-lawyer`](agents/software-lawyer) лежит агент-пресет для DeepSeek Harness: специализированный консультант по правовым вопросам разработки ПО (лицензии, авторские права, персональные данные 152-ФЗ/GDPR, договоры). Отвечает на основе встроенной базы знаний (`skills/software-law/references/`), без внешних API.
|
||||||
|
|||||||
+78
-2
@@ -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) {
|
rpc GetUsers(GetUsersReq) returns (GetUsersRsp) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/api/users"
|
get: "/api/users"
|
||||||
@@ -525,8 +566,10 @@ message EchoRsp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message SignupReq {
|
message SignupReq {
|
||||||
string username = 1;
|
string username = 1;
|
||||||
string email = 2;
|
string email = 2;
|
||||||
|
bool accept_terms = 3;
|
||||||
|
bool accept_privacy = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SignupRsp {
|
message SignupRsp {
|
||||||
@@ -562,6 +605,39 @@ message RefreshRsp {
|
|||||||
string refreshToken = 3;
|
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 GetUsersReq {}
|
||||||
|
|
||||||
message GetUsersRsp {
|
message GetUsersRsp {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
@@ -125,6 +126,9 @@ func main() {
|
|||||||
"/crabs.evening_detective_server.EveningDetectiveServer/GetScenariosCatalog": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/GetScenariosCatalog": true,
|
||||||
"/crabs.evening_detective_server.EveningDetectiveServer/GetTeamStory": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/GetTeamStory": true,
|
||||||
"/crabs.evening_detective_server.EveningDetectiveServer/AddTeamAction": 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,
|
processorJWT,
|
||||||
),
|
),
|
||||||
@@ -158,7 +162,23 @@ func main() {
|
|||||||
log.Fatalln("Failed to dial server:", err)
|
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
|
// Register Greeter
|
||||||
err = proto.RegisterEveningDetectiveServerHandler(context.Background(), gwmux, conn)
|
err = proto.RegisterEveningDetectiveServerHandler(context.Background(), gwmux, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,6 +16,30 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"paths": {
|
"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": {
|
"/api/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Войти и получить токена доступа",
|
"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": {
|
"/api/files/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Загрузить файл",
|
"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": {
|
"/api/scenario": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Создать сценарий",
|
"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": {
|
"/api/test/echo": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Проверить обработку данных",
|
"summary": "Проверить обработку данных",
|
||||||
@@ -1758,6 +1851,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverDeleteAccountRsp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"evening_detective_serverDeleteGameRsp": {
|
"evening_detective_serverDeleteGameRsp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1888,6 +1989,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverGetConsentRsp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"evening_detective_serverGetGameRsp": {
|
"evening_detective_serverGetGameRsp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1954,6 +2066,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverGetPrivacyRsp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"evening_detective_serverGetScenarioRsp": {
|
"evening_detective_serverGetScenarioRsp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1994,6 +2117,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverGetTermsRsp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"evening_detective_serverGetUserByIdRsp": {
|
"evening_detective_serverGetUserByIdRsp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2222,6 +2356,12 @@
|
|||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"acceptTerms": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"acceptPrivacy": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.2–7.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) — по остаточному принципу после запуска.
|
||||||
@@ -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
@@ -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
@@ -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].**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Пользователь подтверждает, что ознакомился с условиями Соглашения, Политикой конфиденциальности, понимает их значение и согласен с ними.*
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -2,6 +2,10 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"evening_detective_server/docs"
|
||||||
"evening_detective_server/internal/modules/file_storage"
|
"evening_detective_server/internal/modules/file_storage"
|
||||||
"evening_detective_server/internal/modules/processor_jwt"
|
"evening_detective_server/internal/modules/processor_jwt"
|
||||||
"evening_detective_server/internal/modules/roles"
|
"evening_detective_server/internal/modules/roles"
|
||||||
@@ -14,6 +18,7 @@ import (
|
|||||||
|
|
||||||
"google.golang.org/genproto/googleapis/api/httpbody"
|
"google.golang.org/genproto/googleapis/api/httpbody"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/grpc/status"
|
"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) {
|
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 {
|
if err != nil {
|
||||||
return &proto.SignupRsp{
|
return &proto.SignupRsp{
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
@@ -61,6 +75,115 @@ func (s *server) Signup(ctx context.Context, req *proto.SignupReq) (*proto.Signu
|
|||||||
return &proto.SignupRsp{}, nil
|
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) {
|
func (s *server) RefreshPassword(ctx context.Context, req *proto.RefreshPasswordReq) (*proto.RefreshPasswordRsp, error) {
|
||||||
err := s.usersService.RefreshPassword(ctx, req.Email)
|
err := s.usersService.RefreshPassword(ctx, req.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -299,6 +422,8 @@ func (s *server) UpdateScenario(ctx context.Context, req *proto.UpdateScenarioRe
|
|||||||
req.Name,
|
req.Name,
|
||||||
req.Description,
|
req.Description,
|
||||||
req.Image,
|
req.Image,
|
||||||
|
claims.UserID,
|
||||||
|
roles.HasRole(claims, roles.Admin),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &proto.UpdateScenarioRsp{
|
return &proto.UpdateScenarioRsp{
|
||||||
@@ -319,6 +444,8 @@ func (s *server) PublicScenario(ctx context.Context, req *proto.PublicScenarioRe
|
|||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
"public",
|
"public",
|
||||||
|
claims.UserID,
|
||||||
|
roles.HasRole(claims, roles.Admin), // публикация = момент отчуждения прав
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &proto.PublicScenarioRsp{
|
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) {
|
func (s *server) DraftScenario(ctx context.Context, req *proto.DraftScenarioReq) (*proto.DraftScenarioRsp, error) {
|
||||||
claims := ctx.Value("claims").(*processor_jwt.JWTClaims)
|
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")
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +469,8 @@ func (s *server) DraftScenario(ctx context.Context, req *proto.DraftScenarioReq)
|
|||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
"draft",
|
"draft",
|
||||||
|
claims.UserID,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &proto.DraftScenarioRsp{
|
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")
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Автор не может удалить опубликованный сценарий (исключительное право
|
||||||
|
// отчуждено); админ может.
|
||||||
err := s.scenarioService.DeleteScenarioByID(
|
err := s.scenarioService.DeleteScenarioByID(
|
||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
|
claims.UserID,
|
||||||
|
roles.HasRole(claims, roles.Admin),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &proto.DeleteScenarioRsp{
|
return &proto.DeleteScenarioRsp{
|
||||||
@@ -378,6 +514,8 @@ func (s *server) AddScenarioPlace(ctx context.Context, req *proto.AddScenarioPla
|
|||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
convertPlace(req.Place),
|
convertPlace(req.Place),
|
||||||
|
claims.UserID,
|
||||||
|
roles.HasRole(claims, roles.Admin),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &proto.AddScenarioPlaceRsp{
|
return &proto.AddScenarioPlaceRsp{
|
||||||
@@ -399,6 +537,8 @@ func (s *server) UpdateScenarioPlace(ctx context.Context, req *proto.UpdateScena
|
|||||||
int(req.Id),
|
int(req.Id),
|
||||||
req.Code,
|
req.Code,
|
||||||
convertPlace(req.Place),
|
convertPlace(req.Place),
|
||||||
|
claims.UserID,
|
||||||
|
roles.HasRole(claims, roles.Admin),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &proto.UpdateScenarioPlaceRsp{
|
return &proto.UpdateScenarioPlaceRsp{
|
||||||
@@ -419,6 +559,8 @@ func (s *server) DeleteScenarioPlace(ctx context.Context, req *proto.DeleteScena
|
|||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
req.Code,
|
req.Code,
|
||||||
|
claims.UserID,
|
||||||
|
roles.HasRole(claims, roles.Admin),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &proto.DeleteScenarioPlaceRsp{
|
return &proto.DeleteScenarioPlaceRsp{
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mapUser преобразует пользователя в proto-структуру. Возвращает nil для
|
||||||
|
// обезличенного автора (аккаунт удалён, контент сохранён) — клиент отображает
|
||||||
|
// такого автора как «Автор удалён».
|
||||||
func mapUser(o *repos.User) *proto.User {
|
func mapUser(o *repos.User) *proto.User {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return &proto.User{
|
return &proto.User{
|
||||||
Id: int32(o.ID),
|
Id: int32(o.ID),
|
||||||
Username: o.Username,
|
Username: o.Username,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -75,17 +75,17 @@ func (r *ScenariosRepo) GetScenariosByAuthorID(
|
|||||||
|
|
||||||
var scenarios []*repos.Scenario
|
var scenarios []*repos.Scenario
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
scenario := &repos.Scenario{
|
scenario := &repos.Scenario{}
|
||||||
Author: &repos.User{},
|
var authorID *int
|
||||||
}
|
var authorUsername *string
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&scenario.ID,
|
&scenario.ID,
|
||||||
&scenario.Name,
|
&scenario.Name,
|
||||||
&scenario.Description,
|
&scenario.Description,
|
||||||
&scenario.Image,
|
&scenario.Image,
|
||||||
&scenario.Scenario,
|
&scenario.Scenario,
|
||||||
&scenario.Author.ID,
|
&authorID,
|
||||||
&scenario.Author.Username,
|
&authorUsername,
|
||||||
&scenario.Status,
|
&scenario.Status,
|
||||||
&scenario.UpdatedAt,
|
&scenario.UpdatedAt,
|
||||||
&scenario.CreatedAt,
|
&scenario.CreatedAt,
|
||||||
@@ -94,6 +94,9 @@ func (r *ScenariosRepo) GetScenariosByAuthorID(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if authorID != nil && authorUsername != nil {
|
||||||
|
scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername}
|
||||||
|
}
|
||||||
scenarios = append(scenarios, scenario)
|
scenarios = append(scenarios, scenario)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
@@ -134,17 +137,17 @@ func (r *ScenariosRepo) GetScenariosByStatus(
|
|||||||
|
|
||||||
var scenarios []*repos.Scenario
|
var scenarios []*repos.Scenario
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
scenario := &repos.Scenario{
|
scenario := &repos.Scenario{}
|
||||||
Author: &repos.User{},
|
var authorID *int
|
||||||
}
|
var authorUsername *string
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&scenario.ID,
|
&scenario.ID,
|
||||||
&scenario.Name,
|
&scenario.Name,
|
||||||
&scenario.Description,
|
&scenario.Description,
|
||||||
&scenario.Image,
|
&scenario.Image,
|
||||||
&scenario.Scenario,
|
&scenario.Scenario,
|
||||||
&scenario.Author.ID,
|
&authorID,
|
||||||
&scenario.Author.Username,
|
&authorUsername,
|
||||||
&scenario.Status,
|
&scenario.Status,
|
||||||
&scenario.UpdatedAt,
|
&scenario.UpdatedAt,
|
||||||
&scenario.CreatedAt,
|
&scenario.CreatedAt,
|
||||||
@@ -153,6 +156,9 @@ func (r *ScenariosRepo) GetScenariosByStatus(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if authorID != nil && authorUsername != nil {
|
||||||
|
scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername}
|
||||||
|
}
|
||||||
scenarios = append(scenarios, scenario)
|
scenarios = append(scenarios, scenario)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
@@ -166,9 +172,9 @@ func (r *ScenariosRepo) GetScenarioByID(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
) (*repos.Scenario, error) {
|
) (*repos.Scenario, error) {
|
||||||
scenario := &repos.Scenario{
|
scenario := &repos.Scenario{}
|
||||||
Author: &repos.User{},
|
var authorID *int
|
||||||
}
|
var authorUsername *string
|
||||||
row := r.pool.QueryRow(
|
row := r.pool.QueryRow(
|
||||||
ctx,
|
ctx,
|
||||||
`SELECT
|
`SELECT
|
||||||
@@ -195,8 +201,8 @@ func (r *ScenariosRepo) GetScenarioByID(
|
|||||||
&scenario.Description,
|
&scenario.Description,
|
||||||
&scenario.Image,
|
&scenario.Image,
|
||||||
&scenario.Scenario,
|
&scenario.Scenario,
|
||||||
&scenario.Author.ID,
|
&authorID,
|
||||||
&scenario.Author.Username,
|
&authorUsername,
|
||||||
&scenario.Status,
|
&scenario.Status,
|
||||||
&scenario.UpdatedAt,
|
&scenario.UpdatedAt,
|
||||||
&scenario.CreatedAt,
|
&scenario.CreatedAt,
|
||||||
@@ -210,9 +216,15 @@ func (r *ScenariosRepo) GetScenarioByID(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Автор может отсутствовать (обезличен после удаления аккаунта).
|
||||||
|
if authorID != nil && authorUsername != nil {
|
||||||
|
scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername}
|
||||||
|
}
|
||||||
|
|
||||||
return scenario, nil
|
return scenario, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateScenarioByID меняет основные поля сценария.
|
||||||
func (r *ScenariosRepo) UpdateScenarioByID(
|
func (r *ScenariosRepo) UpdateScenarioByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
@@ -243,50 +255,61 @@ func (r *ScenariosRepo) UpdateScenarioByID(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateScenarioStatusByID меняет статус сценария. allowPublished=true
|
||||||
|
// разрешает менять статус опубликованного сценария (только для админа).
|
||||||
|
// Возвращает false, если строка не обновлена (например, не-админ пытается
|
||||||
|
// снять с публикации опубликованный сценарий). published_at фиксируется
|
||||||
|
// только при ПЕРВОЙ публикации: повторная публикация не двигает «момент
|
||||||
|
// отчуждения» исключительного права (п. 7.2 Пользовательского соглашения).
|
||||||
func (r *ScenariosRepo) UpdateScenarioStatusByID(
|
func (r *ScenariosRepo) UpdateScenarioStatusByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
status string,
|
status string,
|
||||||
) error {
|
allowPublished bool,
|
||||||
|
) (bool, error) {
|
||||||
tag, err := r.pool.Exec(
|
tag, err := r.pool.Exec(
|
||||||
ctx,
|
ctx,
|
||||||
`UPDATE scenarios
|
`UPDATE scenarios
|
||||||
SET
|
SET
|
||||||
status = $1,
|
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()
|
updated_at = NOW()
|
||||||
WHERE id = $2`,
|
WHERE id = $2 AND ($3 OR status <> 'public')`,
|
||||||
status,
|
status,
|
||||||
id,
|
id,
|
||||||
|
allowPublished,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
if tag.RowsAffected() == 0 {
|
return tag.RowsAffected() > 0, nil
|
||||||
return ErrScenarioNotFound
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteScenarioByID помечает сценарий удалённым (soft delete).
|
||||||
|
// allowPublished=true разрешает удаление опубликованного сценария
|
||||||
|
// (только для админа). Возвращает false, если строка не обновлена
|
||||||
|
// (например, не-админ пытается удалить опубликованный сценарий).
|
||||||
func (r *ScenariosRepo) DeleteScenarioByID(
|
func (r *ScenariosRepo) DeleteScenarioByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
) error {
|
allowPublished bool,
|
||||||
|
) (bool, error) {
|
||||||
tag, err := r.pool.Exec(
|
tag, err := r.pool.Exec(
|
||||||
ctx,
|
ctx,
|
||||||
`UPDATE scenarios
|
`UPDATE scenarios
|
||||||
SET
|
SET
|
||||||
is_deleted = TRUE
|
is_deleted = TRUE
|
||||||
WHERE id = $1`,
|
WHERE id = $1 AND ($2 OR status <> 'public')`,
|
||||||
id,
|
id,
|
||||||
|
allowPublished,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
if tag.RowsAffected() == 0 {
|
return tag.RowsAffected() > 0, nil
|
||||||
return ErrScenarioNotFound
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ScenariosRepo) GetStoryByScenarioID(
|
func (r *ScenariosRepo) GetStoryByScenarioID(
|
||||||
@@ -315,6 +338,7 @@ func (r *ScenariosRepo) GetStoryByScenarioID(
|
|||||||
return story, nil
|
return story, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateStoryByScenarioID обновляет содержимое (историю) сценария.
|
||||||
func (r *ScenariosRepo) UpdateStoryByScenarioID(
|
func (r *ScenariosRepo) UpdateStoryByScenarioID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
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
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"evening_detective_server/internal/repos"
|
"evening_detective_server/internal/repos"
|
||||||
|
"evening_detective_server/internal/repos/user_agreements_repo"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"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,
|
ctx context.Context,
|
||||||
username string,
|
username string,
|
||||||
email string,
|
email string,
|
||||||
passwordHash string,
|
passwordHash string,
|
||||||
roles []string,
|
roles []string,
|
||||||
) error {
|
agreements []user_agreements_repo.Agreement,
|
||||||
tag, err := s.pool.Exec(
|
) (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,
|
ctx,
|
||||||
`INSERT INTO users (username, email, password_hash, roles)
|
`INSERT INTO users (username, email, password_hash, roles)
|
||||||
SELECT $1, $2, $3, $4
|
SELECT $1, $2, $3, $4
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM users
|
SELECT 1 FROM users
|
||||||
WHERE username = $5 OR email = $6
|
WHERE username = $5 OR email = $6
|
||||||
)`,
|
)
|
||||||
|
RETURNING id`,
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
@@ -46,14 +67,41 @@ func (s *UsersRepo) AddUser(
|
|||||||
|
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
)
|
).Scan(&id)
|
||||||
if err != nil {
|
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(
|
func (s *UsersRepo) UpdateUserPassword(
|
||||||
@@ -207,3 +255,62 @@ func (s *UsersRepo) DeleteUserRole(
|
|||||||
)
|
)
|
||||||
return err
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"evening_detective_server/internal/modules/cleaner"
|
"evening_detective_server/internal/modules/cleaner"
|
||||||
"evening_detective_server/internal/modules/storytelling"
|
"evening_detective_server/internal/modules/storytelling"
|
||||||
|
"evening_detective_server/internal/repos"
|
||||||
"evening_detective_server/internal/repos/scenarios_repo"
|
"evening_detective_server/internal/repos/scenarios_repo"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -88,30 +89,111 @@ func (s *ScenarioService) UpdateScenarioByID(
|
|||||||
name string,
|
name string,
|
||||||
description string,
|
description string,
|
||||||
image string,
|
image string,
|
||||||
|
actorId int,
|
||||||
|
isAdmin bool,
|
||||||
) error {
|
) error {
|
||||||
|
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return s.scenariosRepo.UpdateScenarioByID(ctx, id, name, description, image)
|
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(
|
func (s *ScenarioService) UpdateScenarioStatusByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
status string,
|
status string,
|
||||||
|
actorId int,
|
||||||
|
isAdmin bool,
|
||||||
) error {
|
) 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(
|
func (s *ScenarioService) DeleteScenarioByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
|
actorId int,
|
||||||
|
isAdmin bool,
|
||||||
) error {
|
) 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(
|
func (s *ScenarioService) AddScenarioPlace(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
place *storytelling.Place,
|
place *storytelling.Place,
|
||||||
|
actorId int,
|
||||||
|
isAdmin bool,
|
||||||
) error {
|
) error {
|
||||||
|
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
story, err := s.getStory(ctx, id)
|
story, err := s.getStory(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -128,7 +210,12 @@ func (s *ScenarioService) UpdateScenarioPlace(
|
|||||||
id int,
|
id int,
|
||||||
code string,
|
code string,
|
||||||
place *storytelling.Place,
|
place *storytelling.Place,
|
||||||
|
actorId int,
|
||||||
|
isAdmin bool,
|
||||||
) error {
|
) error {
|
||||||
|
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
story, err := s.getStory(ctx, id)
|
story, err := s.getStory(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -146,7 +233,12 @@ func (s *ScenarioService) DeleteScenarioPlace(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int,
|
id int,
|
||||||
code string,
|
code string,
|
||||||
|
actorId int,
|
||||||
|
isAdmin bool,
|
||||||
) error {
|
) error {
|
||||||
|
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
story, err := s.getStory(ctx, id)
|
story, err := s.getStory(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"evening_detective_server/internal/modules/roles"
|
"evening_detective_server/internal/modules/roles"
|
||||||
"evening_detective_server/internal/repos"
|
"evening_detective_server/internal/repos"
|
||||||
"evening_detective_server/internal/repos/refresh_tokens_repo"
|
"evening_detective_server/internal/repos/refresh_tokens_repo"
|
||||||
|
"evening_detective_server/internal/repos/user_agreements_repo"
|
||||||
"evening_detective_server/internal/repos/users_repo"
|
"evening_detective_server/internal/repos/users_repo"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -17,6 +18,17 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"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 {
|
type UsersService struct {
|
||||||
usersRepo *users_repo.UsersRepo
|
usersRepo *users_repo.UsersRepo
|
||||||
passwordGenerator password_generator.IPasswordGenerator
|
passwordGenerator password_generator.IPasswordGenerator
|
||||||
@@ -41,11 +53,22 @@ func NewUsersService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений.
|
||||||
|
var ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности")
|
||||||
|
|
||||||
func (s *UsersService) AddUser(
|
func (s *UsersService) AddUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
username string,
|
username string,
|
||||||
email string,
|
email string,
|
||||||
|
acceptTerms bool,
|
||||||
|
acceptPrivacy bool,
|
||||||
|
ip string,
|
||||||
|
userAgent string,
|
||||||
) error {
|
) error {
|
||||||
|
if !acceptTerms || !acceptPrivacy {
|
||||||
|
return ErrTermsNotAccepted
|
||||||
|
}
|
||||||
|
|
||||||
password, err := s.passwordGenerator.Generate()
|
password, err := s.passwordGenerator.Generate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -54,12 +77,28 @@ func (s *UsersService) AddUser(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.usersRepo.AddUser(
|
// Создание пользователя и фиксация акцептов — в одной транзакции:
|
||||||
|
// без записей о согласиях пользователь не существует.
|
||||||
|
_, err = s.usersRepo.AddUserWithAgreements(
|
||||||
ctx,
|
ctx,
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
string(hashedPassword),
|
string(hashedPassword),
|
||||||
[]string{"user"},
|
[]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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -74,6 +113,24 @@ func (s *UsersService) AddUser(
|
|||||||
return err
|
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(
|
func (s *UsersService) RefreshPassword(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
email string,
|
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
File diff suppressed because it is too large
Load Diff
@@ -199,6 +199,90 @@ func local_request_EveningDetectiveServer_Refresh_0(ctx context.Context, marshal
|
|||||||
return msg, metadata, err
|
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) {
|
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 (
|
var (
|
||||||
protoReq GetUsersReq
|
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()...)
|
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) {
|
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())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
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()...)
|
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) {
|
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())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
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_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_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_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_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_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"}, ""))
|
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_RefreshPassword_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_Login_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_Login_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_Refresh_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_GetUsers_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_GetUserById_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_GetUserById_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_GetMe_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_GetMe_0 = runtime.ForwardResponseMessage
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const (
|
|||||||
EveningDetectiveServer_RefreshPassword_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/RefreshPassword"
|
EveningDetectiveServer_RefreshPassword_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/RefreshPassword"
|
||||||
EveningDetectiveServer_Login_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/Login"
|
EveningDetectiveServer_Login_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/Login"
|
||||||
EveningDetectiveServer_Refresh_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/Refresh"
|
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_GetUsers_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetUsers"
|
||||||
EveningDetectiveServer_GetUserById_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetUserById"
|
EveningDetectiveServer_GetUserById_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetUserById"
|
||||||
EveningDetectiveServer_GetMe_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetMe"
|
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)
|
RefreshPassword(ctx context.Context, in *RefreshPasswordReq, opts ...grpc.CallOption) (*RefreshPasswordRsp, error)
|
||||||
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginRsp, error)
|
Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginRsp, error)
|
||||||
Refresh(ctx context.Context, in *RefreshReq, opts ...grpc.CallOption) (*RefreshRsp, 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)
|
GetUsers(ctx context.Context, in *GetUsersReq, opts ...grpc.CallOption) (*GetUsersRsp, error)
|
||||||
GetUserById(ctx context.Context, in *GetUserByIdReq, opts ...grpc.CallOption) (*GetUserByIdRsp, error)
|
GetUserById(ctx context.Context, in *GetUserByIdReq, opts ...grpc.CallOption) (*GetUserByIdRsp, error)
|
||||||
GetMe(ctx context.Context, in *GetMeReq, opts ...grpc.CallOption) (*GetMeRsp, 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
|
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) {
|
func (c *eveningDetectiveServerClient) GetUsers(ctx context.Context, in *GetUsersReq, opts ...grpc.CallOption) (*GetUsersRsp, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(GetUsersRsp)
|
out := new(GetUsersRsp)
|
||||||
@@ -562,6 +610,10 @@ type EveningDetectiveServerServer interface {
|
|||||||
RefreshPassword(context.Context, *RefreshPasswordReq) (*RefreshPasswordRsp, error)
|
RefreshPassword(context.Context, *RefreshPasswordReq) (*RefreshPasswordRsp, error)
|
||||||
Login(context.Context, *LoginReq) (*LoginRsp, error)
|
Login(context.Context, *LoginReq) (*LoginRsp, error)
|
||||||
Refresh(context.Context, *RefreshReq) (*RefreshRsp, 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)
|
GetUsers(context.Context, *GetUsersReq) (*GetUsersRsp, error)
|
||||||
GetUserById(context.Context, *GetUserByIdReq) (*GetUserByIdRsp, error)
|
GetUserById(context.Context, *GetUserByIdReq) (*GetUserByIdRsp, error)
|
||||||
GetMe(context.Context, *GetMeReq) (*GetMeRsp, 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) {
|
func (UnimplementedEveningDetectiveServerServer) Refresh(context.Context, *RefreshReq) (*RefreshRsp, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method Refresh not implemented")
|
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) {
|
func (UnimplementedEveningDetectiveServerServer) GetUsers(context.Context, *GetUsersReq) (*GetUsersRsp, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetUsers not implemented")
|
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)
|
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) {
|
func _EveningDetectiveServer_GetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(GetUsersReq)
|
in := new(GetUsersReq)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -1565,6 +1701,22 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Refresh",
|
MethodName: "Refresh",
|
||||||
Handler: _EveningDetectiveServer_Refresh_Handler,
|
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",
|
MethodName: "GetUsers",
|
||||||
Handler: _EveningDetectiveServer_GetUsers_Handler,
|
Handler: _EveningDetectiveServer_GetUsers_Handler,
|
||||||
|
|||||||
Reference in New Issue
Block a user