diff --git a/README.md b/README.md index de973e0..fa95317 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ make test ## Документация и юридические документы -- [`docs/TERMS.md`](docs/TERMS.md) — Пользовательское соглашение (оферта); раздаётся сервером по постоянному URL `/terms`; -- [`docs/PRIVACY.md`](docs/PRIVACY.md) — Политика конфиденциальности и обработки персональных данных (152-ФЗ); раздаётся сервером по постоянному URL `/privacy`; -- [`docs/CONSENT.md`](docs/CONSENT.md) — текст согласия на обработку ПДн для формы регистрации (URL `/consent`); +- [`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) — чек-лист приведения сервиса в соответствие с документами. Юридические эндпоинты: diff --git a/api/main.proto b/api/main.proto index 803b2f4..7502a81 100644 --- a/api/main.proto +++ b/api/main.proto @@ -128,7 +128,7 @@ service EveningDetectiveServer { rpc GetTerms(GetTermsReq) returns (GetTermsRsp) { option (google.api.http) = { - get: "/terms" + get: "/api/terms" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Документы"; @@ -138,7 +138,7 @@ service EveningDetectiveServer { rpc GetPrivacy(GetPrivacyReq) returns (GetPrivacyRsp) { option (google.api.http) = { - get: "/privacy" + get: "/api/privacy" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Документы"; @@ -148,7 +148,7 @@ service EveningDetectiveServer { rpc GetConsent(GetConsentReq) returns (GetConsentRsp) { option (google.api.http) = { - get: "/consent" + get: "/api/consent" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Документы"; diff --git a/cmd/evening_detective_server/main.swagger.json b/cmd/evening_detective_server/main.swagger.json index 48232ac..d7d9b9f 100644 --- a/cmd/evening_detective_server/main.swagger.json +++ b/cmd/evening_detective_server/main.swagger.json @@ -215,6 +215,29 @@ ] } }, + "/api/consent": { + "get": { + "summary": "Получить текст Согласия на обработку персональных данных", + "operationId": "EveningDetectiveServer_GetConsent", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/evening_detective_serverGetConsentRsp" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "Документы" + ] + } + }, "/api/files/upload": { "post": { "summary": "Загрузить файл", @@ -656,6 +679,29 @@ ] } }, + "/api/privacy": { + "get": { + "summary": "Получить текст Политики конфиденциальности", + "operationId": "EveningDetectiveServer_GetPrivacy", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/evening_detective_serverGetPrivacyRsp" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "Документы" + ] + } + }, "/api/scenario": { "post": { "summary": "Создать сценарий", @@ -1286,6 +1332,29 @@ ] } }, + "/api/terms": { + "get": { + "summary": "Получить текст Пользовательского соглашения (оферты)", + "operationId": "EveningDetectiveServer_GetTerms", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/evening_detective_serverGetTermsRsp" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "Документы" + ] + } + }, "/api/test/echo": { "post": { "summary": "Проверить обработку данных", @@ -1530,75 +1599,6 @@ "Пользователи" ] } - }, - "/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": [ - "Документы" - ] - } - }, - "/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": [ - "Документы" - ] - } - }, - "/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": [ - "Документы" - ] - } } }, "definitions": { diff --git a/docs/COMPLIANCE.md b/docs/COMPLIANCE.md index 094990f..9a5ca75 100644 --- a/docs/COMPLIANCE.md +++ b/docs/COMPLIANCE.md @@ -15,7 +15,7 @@ | 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`, пути `/terms`, `/privacy`, `/consent`); текст встраивается из `docs/embed.go`; подставить реальные домены в тексты документов | +| 1.4 | Публикация документов на сайте по постоянным URL | ✅ | RPC `GetTerms`/`GetPrivacy`/`GetConsent` (`api/main.proto`, пути `/api/terms`, `/api/privacy`, `/api/consent`); текст встраивается из `docs/embed.go`; подставить реальные домены в тексты документов | ## 2. Акцепт соглашений при регистрации (ст. 428 ГК РФ, ст. 9 152-ФЗ) @@ -116,7 +116,7 @@ ## Порядок действий перед запуском в прод 1. Заполнить плейсхолдеры в `docs/TERMS.md`, `docs/PRIVACY.md`, `docs/CONSENT.md` и утвердить у юриста. -2. Вставить в тексты документов реальные домены (`/terms`, `/privacy`, `/consent`); вставить чекбоксы акцепта, подтверждение отчуждения прав при публикации сценария и кнопку «Удалить аккаунт» во фронтенд. +2. Вставить в тексты документов реальные домены (`/api/terms`, `/api/privacy`, `/api/consent`); вставить чекбоксы акцепта, подтверждение отчуждения прав при публикации сценария и кнопку «Удалить аккаунт» во фронтенд. 3. Подать уведомление в Роскомнадзор (п. 4.2) до начала обработки. 4. Проверить локализацию: БД, S3, SMTP — в РФ. 5. Прогнать `goose up` на staging; добавить интеграционный тест (п. 8.3) — включая сценарий: публикация → удаление запрещено; удаление аккаунта → сценарий остаётся с `author: null`. diff --git a/internal/app/server.go b/internal/app/server.go index aea5d1f..719c4e2 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -99,7 +99,7 @@ func (s *server) DeleteAccount(ctx context.Context, req *proto.DeleteAccountReq) } // GetTerms возвращает текст Пользовательского соглашения (постоянный URL -// /terms, на который ссылается форма регистрации при акцепте). +// /api/terms, на который ссылается форма регистрации при акцепте). func (s *server) GetTerms(_ context.Context, _ *proto.GetTermsReq) (*proto.GetTermsRsp, error) { text, err := docs.FS.ReadFile("TERMS.md") if err != nil { @@ -109,7 +109,7 @@ func (s *server) GetTerms(_ context.Context, _ *proto.GetTermsReq) (*proto.GetTe } // GetPrivacy возвращает текст Политики конфиденциальности (постоянный URL -// /privacy). +// /api/privacy). func (s *server) GetPrivacy(_ context.Context, _ *proto.GetPrivacyReq) (*proto.GetPrivacyRsp, error) { text, err := docs.FS.ReadFile("PRIVACY.md") if err != nil { @@ -119,7 +119,7 @@ func (s *server) GetPrivacy(_ context.Context, _ *proto.GetPrivacyReq) (*proto.G } // GetConsent возвращает текст согласия на обработку персональных данных -// (постоянный URL /consent). +// (постоянный URL /api/consent). func (s *server) GetConsent(_ context.Context, _ *proto.GetConsentReq) (*proto.GetConsentRsp, error) { text, err := docs.FS.ReadFile("CONSENT.md") if err != nil { diff --git a/internal/services/users_service/service.go b/internal/services/users_service/service.go index 8681a0d..cfb2a32 100644 --- a/internal/services/users_service/service.go +++ b/internal/services/users_service/service.go @@ -19,7 +19,7 @@ import ( ) // Версии документов, акцепт которых фиксируется. Должны совпадать с -// редакциями, опубликованными по постоянным URL (/terms и /privacy, +// редакциями, опубликованными по постоянным URL (/api/terms и /api/privacy, // rpc GetTerms/GetPrivacy, текст встраивается из пакета docs): при изменении // текста документа версия ОБЯЗАНА увеличиваться, иначе повторный акцепт // не будет зафиксирован как согласие с новой редакцией diff --git a/proto/main.pb.go b/proto/main.pb.go index fbcc610..4d277c0 100644 --- a/proto/main.pb.go +++ b/proto/main.pb.go @@ -5331,7 +5331,7 @@ const file_main_proto_rawDesc = "" + "\x17DeleteLastTeamActionReq\x12\x0e\n" + "\x02id\x18\x01 \x01(\x03R\x02id\"/\n" + "\x17DeleteLastTeamActionRsp\x12\x14\n" + - "\x05error\x18\x01 \x01(\tR\x05error2\xc8O\n" + + "\x05error\x18\x01 \x01(\tR\x05error2\xd4O\n" + "\x16EveningDetectiveServer\x12\xa6\x01\n" + "\x04Ping\x12'.crabs.evening_detective_server.PingReq\x1a'.crabs.evening_detective_server.PingRsp\"L\x92A3\x12)Проверить доступностьb\x06\n" + "\x04\n" + @@ -5356,17 +5356,16 @@ const file_main_proto_rawDesc = "" + "\x04\n" + "\x00\x12\x00\x82\xd3\xe4\x93\x02\x16:\x01*\"\x11/api/auth/refresh\x12\xe3\x04\n" + "\rDeleteAccount\x120.crabs.evening_detective_server.DeleteAccountReq\x1a0.crabs.evening_detective_server.DeleteAccountRsp\"\xed\x03\x92A\xc9\x03\n" + - "\"Регистрация и вход\x12SУдалить учётную запись и персональные данные\x1a\xcd\x02Удаляет учётную запись и персональные данные текущего пользователя (ст. 21 152-ФЗ). Требует заголовок X-Password с паролем учётной записи (подтверждение владельца; пароль не передаётся в URL).\x82\xd3\xe4\x93\x02\x1a*\x18/api/auth/delete-account\x12\xf0\x01\n" + - "\bGetTerms\x12+.crabs.evening_detective_server.GetTermsReq\x1a+.crabs.evening_detective_server.GetTermsRsp\"\x89\x01\x92Ax\n" + - "\x12Документы\x12bПолучить текст Пользовательского соглашения (оферты)\x82\xd3\xe4\x93\x02\b\x12\x06/terms\x12\xe6\x01\n" + + "\"Регистрация и вход\x12SУдалить учётную запись и персональные данные\x1a\xcd\x02Удаляет учётную запись и персональные данные текущего пользователя (ст. 21 152-ФЗ). Требует заголовок X-Password с паролем учётной записи (подтверждение владельца; пароль не передаётся в URL).\x82\xd3\xe4\x93\x02\x1a*\x18/api/auth/delete-account\x12\xf4\x01\n" + + "\bGetTerms\x12+.crabs.evening_detective_server.GetTermsReq\x1a+.crabs.evening_detective_server.GetTermsRsp\"\x8d\x01\x92Ax\n" + + "\x12Документы\x12bПолучить текст Пользовательского соглашения (оферты)\x82\xd3\xe4\x93\x02\f\x12\n" + + "/api/terms\x12\xea\x01\n" + "\n" + - "GetPrivacy\x12-.crabs.evening_detective_server.GetPrivacyReq\x1a-.crabs.evening_detective_server.GetPrivacyRsp\"z\x92Ag\n" + - "\x12Документы\x12QПолучить текст Политики конфиденциальности\x82\xd3\xe4\x93\x02\n" + - "\x12\b/privacy\x12\x81\x02\n" + + "GetPrivacy\x12-.crabs.evening_detective_server.GetPrivacyReq\x1a-.crabs.evening_detective_server.GetPrivacyRsp\"~\x92Ag\n" + + "\x12Документы\x12QПолучить текст Политики конфиденциальности\x82\xd3\xe4\x93\x02\x0e\x12\f/api/privacy\x12\x85\x02\n" + "\n" + - "GetConsent\x12-.crabs.evening_detective_server.GetConsentReq\x1a-.crabs.evening_detective_server.GetConsentRsp\"\x94\x01\x92A\x80\x01\n" + - "\x12Документы\x12jПолучить текст согласия на обработку персональных данных\x82\xd3\xe4\x93\x02\n" + - "\x12\b/consent\x12\xcb\x01\n" + + "GetConsent\x12-.crabs.evening_detective_server.GetConsentReq\x1a-.crabs.evening_detective_server.GetConsentRsp\"\x98\x01\x92A\x80\x01\n" + + "\x12Документы\x12jПолучить текст Согласия на обработку персональных данных\x82\xd3\xe4\x93\x02\x0e\x12\f/api/consent\x12\xcb\x01\n" + "\bGetUsers\x12+.crabs.evening_detective_server.GetUsersReq\x1a+.crabs.evening_detective_server.GetUsersRsp\"e\x92AP\n" + "\x18Пользователи\x124Получить всех пользователей\x82\xd3\xe4\x93\x02\f\x12\n" + "/api/users\x12\xd6\x01\n" + diff --git a/proto/main.pb.gw.go b/proto/main.pb.gw.go index bcc7eb7..1bc6cdd 100644 --- a/proto/main.pb.gw.go +++ b/proto/main.pb.gw.go @@ -1864,7 +1864,7 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti 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("/terms")) + 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 @@ -1884,7 +1884,7 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti 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("/privacy")) + 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 @@ -1904,7 +1904,7 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti 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("/consent")) + 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 @@ -2821,7 +2821,7 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti 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("/terms")) + 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 @@ -2838,7 +2838,7 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti 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("/privacy")) + 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 @@ -2855,7 +2855,7 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti 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("/consent")) + 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 @@ -3508,9 +3508,9 @@ var ( pattern_EveningDetectiveServer_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "login"}, "")) pattern_EveningDetectiveServer_Refresh_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "refresh"}, "")) pattern_EveningDetectiveServer_DeleteAccount_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "delete-account"}, "")) - pattern_EveningDetectiveServer_GetTerms_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"terms"}, "")) - pattern_EveningDetectiveServer_GetPrivacy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"privacy"}, "")) - pattern_EveningDetectiveServer_GetConsent_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"consent"}, "")) + pattern_EveningDetectiveServer_GetTerms_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "terms"}, "")) + pattern_EveningDetectiveServer_GetPrivacy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "privacy"}, "")) + pattern_EveningDetectiveServer_GetConsent_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "consent"}, "")) pattern_EveningDetectiveServer_GetUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "users"}, "")) pattern_EveningDetectiveServer_GetUserById_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "users", "id"}, "")) pattern_EveningDetectiveServer_GetMe_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "users", "me"}, ""))