This commit is contained in:
2026-08-20 01:57:39 +07:00
parent bb8fca352a
commit 862c918981
15 changed files with 879 additions and 287 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вечерний детектив</title>
<script type="module" crossorigin src="/assets/index-B54R2K0c.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BdDl0UCc.css">
<script type="module" crossorigin src="/assets/index-BhT0EYtM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DoB8eiys.css">
</head>
<body>
<div id="app"></div>
+78 -2
View File
@@ -115,6 +115,47 @@ service EveningDetectiveServer {
};
}
rpc DeleteAccount(DeleteAccountReq) returns (DeleteAccountRsp) {
option (google.api.http) = {
delete: "/api/auth/delete-account"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Регистрация и вход";
summary : "Удалить учётную запись и персональные данные";
description: "Удаляет учётную запись и персональные данные текущего пользователя (ст. 21 152-ФЗ). Требует заголовок X-Password с паролем учётной записи (подтверждение владельца; пароль не передаётся в URL).";
};
}
rpc GetTerms(GetTermsReq) returns (GetTermsRsp) {
option (google.api.http) = {
get: "/api/terms"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Документы";
summary: "Получить текст Пользовательского соглашения (оферты)";
};
}
rpc GetPrivacy(GetPrivacyReq) returns (GetPrivacyRsp) {
option (google.api.http) = {
get: "/api/privacy"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Документы";
summary: "Получить текст Политики конфиденциальности";
};
}
rpc GetConsent(GetConsentReq) returns (GetConsentRsp) {
option (google.api.http) = {
get: "/api/consent"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Документы";
summary: "Получить текст Согласия на обработку персональных данных";
};
}
rpc GetUsers(GetUsersReq) returns (GetUsersRsp) {
option (google.api.http) = {
get: "/api/users"
@@ -525,8 +566,10 @@ message EchoRsp {
}
message SignupReq {
string username = 1;
string email = 2;
string username = 1;
string email = 2;
bool accept_terms = 3;
bool accept_privacy = 4;
}
message SignupRsp {
@@ -562,6 +605,39 @@ message RefreshRsp {
string refreshToken = 3;
}
// Пароль подтверждает, что удаляет аккаунт его владелец, а не обладатель
// украденного access-токена (ст. 21 152-ФЗ). Передаётся HTTP-заголовком
// X-Password (пробрасывается матчером в cmd/evening_detective_server/main.go),
// чтобы креденшел не попадал в URL/query-параметры и логи.
message DeleteAccountReq {}
message DeleteAccountRsp {
string error = 1;
}
// Документы раздаются по постоянным URL, на которые ссылается сервис при
// фиксации акцептов соглашений (user_agreements) и форма регистрации.
message GetTermsReq {}
message GetTermsRsp {
string error = 1;
string text = 2;
}
message GetPrivacyReq {}
message GetPrivacyRsp {
string error = 1;
string text = 2;
}
message GetConsentReq {}
message GetConsentRsp {
string error = 1;
string text = 2;
}
message GetUsersReq {}
message GetUsersRsp {
@@ -1,11 +1,17 @@
import { createEveningDetectiveServerClient, type EveningDetectiveServer } from '.'
export function buildClient(getToken?: () => string | null): EveningDetectiveServer {
return createEveningDetectiveServerClient(createHttpHandler(getToken))
export type ExtraHeadersProvider = () => Record<string, string> | null | undefined
export function buildClient(
getToken?: () => string | null,
getExtraHeaders?: ExtraHeadersProvider,
): EveningDetectiveServer {
return createEveningDetectiveServerClient(createHttpHandler(getToken, getExtraHeaders))
}
export function createHttpHandler(
getToken?: () => string | null,
getExtraHeaders?: ExtraHeadersProvider,
): (request: { path: string; method: string; body: string | null }) => Promise<unknown> {
return async (request: { path: string; method: string; body: string | null }) => {
const baseURL = getURL()
@@ -24,6 +30,12 @@ export function createHttpHandler(
headers['Authorization'] = `Bearer ${token}`
}
// Добавляем дополнительные заголовки (например, X-Password для удаления аккаунта)
const extraHeaders = getExtraHeaders?.()
if (extraHeaders) {
Object.assign(headers, extraHeaders)
}
// Делаем запрос
const response = await fetch(url, {
method: request.method,
@@ -1,5 +1,5 @@
// Code generated by protoc-gen-typescript-http. DO NOT EDIT.
/* eslint-disable camelcase */
// @ts-nocheck
export type PingReq = {
@@ -19,6 +19,8 @@ export type EchoRsp = {
export type SignupReq = {
username: string | undefined;
email: string | undefined;
acceptTerms: boolean | undefined;
acceptPrivacy: boolean | undefined;
};
export type SignupRsp = {
@@ -54,6 +56,43 @@ export type RefreshRsp = {
refreshToken: string | undefined;
};
// Пароль подтверждает, что удаляет аккаунт его владелец, а не обладатель
// украденного access-токена (ст. 21 152-ФЗ). Передаётся HTTP-заголовком
// X-Password (пробрасывается матчером в cmd/evening_detective_server/main.go),
// чтобы креденшел не попадал в URL/query-параметры и логи.
export type DeleteAccountReq = {
};
export type DeleteAccountRsp = {
error: string | undefined;
};
// Документы раздаются по постоянным URL, на которые ссылается сервис при
// фиксации акцептов соглашений (user_agreements) и форма регистрации.
export type GetTermsReq = {
};
export type GetTermsRsp = {
error: string | undefined;
text: string | undefined;
};
export type GetPrivacyReq = {
};
export type GetPrivacyRsp = {
error: string | undefined;
text: string | undefined;
};
export type GetConsentReq = {
};
export type GetConsentRsp = {
error: string | undefined;
text: string | undefined;
};
export type GetUsersReq = {
};
@@ -456,6 +495,10 @@ export interface EveningDetectiveServer {
RefreshPassword(request: RefreshPasswordReq): Promise<RefreshPasswordRsp>;
Login(request: LoginReq): Promise<LoginRsp>;
Refresh(request: RefreshReq): Promise<RefreshRsp>;
DeleteAccount(request: DeleteAccountReq): Promise<DeleteAccountRsp>;
GetTerms(request: GetTermsReq): Promise<GetTermsRsp>;
GetPrivacy(request: GetPrivacyReq): Promise<GetPrivacyRsp>;
GetConsent(request: GetConsentReq): Promise<GetConsentRsp>;
GetUsers(request: GetUsersReq): Promise<GetUsersRsp>;
GetUserById(request: GetUserByIdReq): Promise<GetUserByIdRsp>;
GetMe(request: GetMeReq): Promise<GetMeRsp>;
@@ -507,8 +550,8 @@ export function createEveningDetectiveServerClient(
handler: RequestHandler
): EveningDetectiveServer {
return {
Ping(request) {
const path = `api/test/ping`;
Ping(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/test/ping`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -524,8 +567,8 @@ export function createEveningDetectiveServerClient(
method: "Ping",
}) as Promise<PingRsp>;
},
Echo(request) {
const path = `api/test/echo`;
Echo(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/test/echo`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
if (request.text) {
@@ -544,8 +587,8 @@ export function createEveningDetectiveServerClient(
method: "Echo",
}) as Promise<EchoRsp>;
},
Signup(request) {
const path = `api/auth/signup`;
Signup(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/signup`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -561,8 +604,8 @@ export function createEveningDetectiveServerClient(
method: "Signup",
}) as Promise<SignupRsp>;
},
RefreshPassword(request) {
const path = `api/auth/refresh-password`;
RefreshPassword(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/refresh-password`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -578,8 +621,8 @@ export function createEveningDetectiveServerClient(
method: "RefreshPassword",
}) as Promise<RefreshPasswordRsp>;
},
Login(request) {
const path = `api/auth/login`;
Login(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/login`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -595,8 +638,8 @@ export function createEveningDetectiveServerClient(
method: "Login",
}) as Promise<LoginRsp>;
},
Refresh(request) {
const path = `api/auth/refresh`;
Refresh(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/refresh`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -612,8 +655,76 @@ export function createEveningDetectiveServerClient(
method: "Refresh",
}) as Promise<RefreshRsp>;
},
GetUsers(request) {
const path = `api/users`;
DeleteAccount(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/delete-account`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "DELETE",
body,
}, {
service: "EveningDetectiveServer",
method: "DeleteAccount",
}) as Promise<DeleteAccountRsp>;
},
GetTerms(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/terms`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "GET",
body,
}, {
service: "EveningDetectiveServer",
method: "GetTerms",
}) as Promise<GetTermsRsp>;
},
GetPrivacy(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/privacy`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "GET",
body,
}, {
service: "EveningDetectiveServer",
method: "GetPrivacy",
}) as Promise<GetPrivacyRsp>;
},
GetConsent(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/consent`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "GET",
body,
}, {
service: "EveningDetectiveServer",
method: "GetConsent",
}) as Promise<GetConsentRsp>;
},
GetUsers(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/users`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -629,11 +740,11 @@ export function createEveningDetectiveServerClient(
method: "GetUsers",
}) as Promise<GetUsersRsp>;
},
GetUserById(request) {
GetUserById(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/users/${request.id}`;
const path = `api/users/${request.id}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -649,8 +760,8 @@ export function createEveningDetectiveServerClient(
method: "GetUserById",
}) as Promise<GetUserByIdRsp>;
},
GetMe(request) {
const path = `api/users/me`;
GetMe(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/users/me`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -666,11 +777,11 @@ export function createEveningDetectiveServerClient(
method: "GetMe",
}) as Promise<GetMeRsp>;
},
AddUserRole(request) {
AddUserRole(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/users/${request.id}/role/add`;
const path = `api/users/${request.id}/role/add`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -686,11 +797,11 @@ export function createEveningDetectiveServerClient(
method: "AddUserRole",
}) as Promise<AddUserRoleRsp>;
},
DeleteUserRole(request) {
DeleteUserRole(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/users/${request.id}/role/delete`;
const path = `api/users/${request.id}/role/delete`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -706,8 +817,8 @@ export function createEveningDetectiveServerClient(
method: "DeleteUserRole",
}) as Promise<DeleteUserRoleRsp>;
},
GetPermissions(request) {
const path = `api/ui/permissions`;
GetPermissions(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/ui/permissions`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -723,8 +834,8 @@ export function createEveningDetectiveServerClient(
method: "GetPermissions",
}) as Promise<GetPermissionsRsp>;
},
UploadFile(request) {
const path = `api/files/upload`;
UploadFile(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/files/upload`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -740,11 +851,11 @@ export function createEveningDetectiveServerClient(
method: "UploadFile",
}) as Promise<UploadFileRsp>;
},
DownloadFile(request) {
DownloadFile(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.filename) {
throw new Error("missing required field request.filename");
}
const path = `api/files/${request.filename}`;
const path = `api/files/${request.filename}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -760,8 +871,8 @@ export function createEveningDetectiveServerClient(
method: "DownloadFile",
}) as Promise<googleapi_HttpBody>;
},
AddScenario(request) {
const path = `api/scenario`;
AddScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/scenario`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -777,8 +888,8 @@ export function createEveningDetectiveServerClient(
method: "AddScenario",
}) as Promise<AddScenarioRsp>;
},
GetMyScenarios(request) {
const path = `api/my-scenarios`;
GetMyScenarios(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/my-scenarios`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -794,8 +905,8 @@ export function createEveningDetectiveServerClient(
method: "GetMyScenarios",
}) as Promise<GetMyScenariosRsp>;
},
GetScenariosCatalog(request) {
const path = `api/catalog`;
GetScenariosCatalog(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/catalog`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -811,11 +922,11 @@ export function createEveningDetectiveServerClient(
method: "GetScenariosCatalog",
}) as Promise<GetScenariosCatalogRsp>;
},
GetFullScenario(request) {
GetFullScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}/full`;
const path = `api/scenarios/${request.id}/full`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -831,11 +942,11 @@ export function createEveningDetectiveServerClient(
method: "GetFullScenario",
}) as Promise<GetScenarioRsp>;
},
GetScenario(request) {
GetScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}`;
const path = `api/scenarios/${request.id}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -851,11 +962,11 @@ export function createEveningDetectiveServerClient(
method: "GetScenario",
}) as Promise<GetScenarioRsp>;
},
UpdateScenario(request) {
UpdateScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}`;
const path = `api/scenarios/${request.id}`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -871,11 +982,11 @@ export function createEveningDetectiveServerClient(
method: "UpdateScenario",
}) as Promise<UpdateScenarioRsp>;
},
PublicScenario(request) {
PublicScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}/public`;
const path = `api/scenarios/${request.id}/public`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -891,11 +1002,11 @@ export function createEveningDetectiveServerClient(
method: "PublicScenario",
}) as Promise<PublicScenarioRsp>;
},
DraftScenario(request) {
DraftScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}/draft`;
const path = `api/scenarios/${request.id}/draft`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -911,11 +1022,11 @@ export function createEveningDetectiveServerClient(
method: "DraftScenario",
}) as Promise<DraftScenarioRsp>;
},
DeleteScenario(request) {
DeleteScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}`;
const path = `api/scenarios/${request.id}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -931,11 +1042,11 @@ export function createEveningDetectiveServerClient(
method: "DeleteScenario",
}) as Promise<DeleteScenarioRsp>;
},
AddScenarioPlace(request) {
AddScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}/places`;
const path = `api/scenarios/${request.id}/places`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -951,14 +1062,14 @@ export function createEveningDetectiveServerClient(
method: "AddScenarioPlace",
}) as Promise<AddScenarioPlaceRsp>;
},
UpdateScenarioPlace(request) {
UpdateScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
if (!request.code) {
throw new Error("missing required field request.code");
}
const path = `api/scenarios/${request.id}/places/${request.code}`;
const path = `api/scenarios/${request.id}/places/${request.code}`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -974,14 +1085,14 @@ export function createEveningDetectiveServerClient(
method: "UpdateScenarioPlace",
}) as Promise<UpdateScenarioPlaceRsp>;
},
DeleteScenarioPlace(request) {
DeleteScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
if (!request.code) {
throw new Error("missing required field request.code");
}
const path = `api/scenarios/${request.id}/places/${request.code}`;
const path = `api/scenarios/${request.id}/places/${request.code}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -997,8 +1108,8 @@ export function createEveningDetectiveServerClient(
method: "DeleteScenarioPlace",
}) as Promise<DeleteScenarioPlaceRsp>;
},
AddGame(request) {
const path = `api/games`;
AddGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/games`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1014,8 +1125,8 @@ export function createEveningDetectiveServerClient(
method: "AddGame",
}) as Promise<AddGameRsp>;
},
GetGames(request) {
const path = `api/games`;
GetGames(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/games`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -1031,11 +1142,11 @@ export function createEveningDetectiveServerClient(
method: "GetGames",
}) as Promise<GetGamesRsp>;
},
GetGame(request) {
GetGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}`;
const path = `api/games/${request.id}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -1051,11 +1162,11 @@ export function createEveningDetectiveServerClient(
method: "GetGame",
}) as Promise<GetGameRsp>;
},
UpdateGame(request) {
UpdateGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}`;
const path = `api/games/${request.id}`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1071,11 +1182,11 @@ export function createEveningDetectiveServerClient(
method: "UpdateGame",
}) as Promise<UpdateGameRsp>;
},
StartGame(request) {
StartGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/start`;
const path = `api/games/${request.id}/start`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1091,11 +1202,11 @@ export function createEveningDetectiveServerClient(
method: "StartGame",
}) as Promise<StartGameRsp>;
},
PauseGame(request) {
PauseGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/pause`;
const path = `api/games/${request.id}/pause`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1111,11 +1222,11 @@ export function createEveningDetectiveServerClient(
method: "PauseGame",
}) as Promise<PauseGameRsp>;
},
PlayGame(request) {
PlayGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/play`;
const path = `api/games/${request.id}/play`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1131,11 +1242,11 @@ export function createEveningDetectiveServerClient(
method: "PlayGame",
}) as Promise<PlayGameRsp>;
},
FinishGame(request) {
FinishGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/finish`;
const path = `api/games/${request.id}/finish`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1151,11 +1262,11 @@ export function createEveningDetectiveServerClient(
method: "FinishGame",
}) as Promise<FinishGameRsp>;
},
ResetGame(request) {
ResetGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/reset`;
const path = `api/games/${request.id}/reset`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1171,11 +1282,11 @@ export function createEveningDetectiveServerClient(
method: "ResetGame",
}) as Promise<ResetGameRsp>;
},
DeleteGame(request) {
DeleteGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}`;
const path = `api/games/${request.id}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -1191,8 +1302,8 @@ export function createEveningDetectiveServerClient(
method: "DeleteGame",
}) as Promise<DeleteGameRsp>;
},
AddTeam(request) {
const path = `api/teams`;
AddTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/teams`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1208,11 +1319,11 @@ export function createEveningDetectiveServerClient(
method: "AddTeam",
}) as Promise<AddTeamRsp>;
},
UpdateTeam(request) {
UpdateTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/teams/${request.id}`;
const path = `api/teams/${request.id}`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1228,11 +1339,11 @@ export function createEveningDetectiveServerClient(
method: "UpdateTeam",
}) as Promise<UpdateTeamRsp>;
},
DeleteTeam(request) {
DeleteTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/teams/${request.id}`;
const path = `api/teams/${request.id}`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
@@ -1248,11 +1359,11 @@ export function createEveningDetectiveServerClient(
method: "DeleteTeam",
}) as Promise<DeleteTeamRsp>;
},
GiveTeamApplications(request) {
GiveTeamApplications(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/teams/${request.id}/applications`;
const path = `api/teams/${request.id}/applications`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1268,11 +1379,11 @@ export function createEveningDetectiveServerClient(
method: "GiveTeamApplications",
}) as Promise<GiveTeamApplicationsRsp>;
},
GetTeamStory(request) {
GetTeamStory(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/teams/${request.id}/story`;
const path = `api/teams/${request.id}/story`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
if (request.password) {
@@ -1291,11 +1402,11 @@ export function createEveningDetectiveServerClient(
method: "GetTeamStory",
}) as Promise<GetTeamStoryRsp>;
},
AddTeamAction(request) {
AddTeamAction(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/teams/${request.id}/actions`;
const path = `api/teams/${request.id}/actions`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
@@ -1311,11 +1422,11 @@ export function createEveningDetectiveServerClient(
method: "AddTeamAction",
}) as Promise<AddTeamActionRsp>;
},
DeleteLastTeamAction(request) {
DeleteLastTeamAction(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/teams/${request.id}/last-actions`;
const path = `api/teams/${request.id}/last-actions`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
+173
View File
@@ -0,0 +1,173 @@
<script setup lang="ts">
import { NAlert, NSpin } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'
import HeaderText from '@/components/HeaderText.vue'
const props = defineProps<{
kind: 'terms' | 'privacy' | 'consent'
}>()
const titles: Record<string, string> = {
terms: 'Пользовательское соглашение',
privacy: 'Политика конфиденциальности',
consent: 'Согласие на обработку персональных данных',
}
const text = ref('')
const loading = ref(true)
const error = ref('')
onMounted(async () => {
const client = getAuthClient()
const fetchers = {
terms: () => client.GetTerms({}),
privacy: () => client.GetPrivacy({}),
consent: () => client.GetConsent({}),
}
try {
const res = await fetchers[props.kind]()
if (res.error) {
error.value = res.error
} else {
text.value = res.text || ''
}
} catch (err) {
error.value = err instanceof Error ? err.message : 'Не удалось загрузить документ'
} finally {
loading.value = false
}
})
</script>
<template>
<div class="center-block-custom">
<div class="width700 doc-page">
<h1 class="doc-title">
<HeaderText>{{ titles[props.kind] }}</HeaderText>
</h1>
<div v-if="loading" class="doc-loading">
<n-spin size="large" />
</div>
<n-alert v-else-if="error" type="error" :show-icon="false" class="doc-error">
{{ error }}
</n-alert>
<div v-else class="paper-block">
<div class="paper">
<p class="doc-text">{{ text }}</p>
</div>
<div class="paper-shadow paper-shadow-left"></div>
<div class="paper-shadow paper-shadow-right"></div>
</div>
<div class="docs-links">
Смотрите также:
<RouterLink to="/user-agreement" class="docs-link">Пользовательское соглашение</RouterLink>
·
<RouterLink to="/privacy-policy" class="docs-link">Политика конфиденциальности</RouterLink>
·
<RouterLink to="/consent" class="docs-link">Согласие на обработку персональных данных</RouterLink>
</div>
</div>
</div>
</template>
<style scoped>
.doc-page {
padding-top: 30px;
}
.doc-title {
margin: 0 0 24px;
text-align: center;
}
.doc-loading {
display: flex;
justify-content: center;
padding: 80px 0;
}
.doc-error {
margin: 0 auto;
max-width: 600px;
}
.paper-block {
position: relative;
}
.paper {
position: relative;
z-index: 2;
padding: 32px 28px;
border-radius: 3px;
background-image: url('@/assets/images/paper_white.jpg');
background-size: cover;
color: #1d1d1d;
box-shadow: 0 0 12px rgb(0 0 0 / 70%);
max-height: 60vh;
overflow-y: auto;
scrollbar-width: none;
}
.paper::-webkit-scrollbar {
display: none;
}
.doc-text {
font-family: 'font_old_typer';
font-size: 17px;
line-height: 1.65;
white-space: pre-wrap;
word-break: break-word;
text-align: justify;
}
.paper-shadow {
position: absolute;
top: 0;
width: 100%;
height: 100%;
border-radius: 3px;
background-image: url('@/assets/images/paper_white.jpg');
background-size: cover;
}
.paper-shadow-left {
left: 0;
transform: rotate(-2.2deg);
filter: brightness(60%);
box-shadow: 0 0 10px rgb(0 0 0 / 60%);
}
.paper-shadow-right {
right: 0;
transform: rotate(2.2deg);
filter: brightness(75%);
box-shadow: 0 0 10px rgb(0 0 0 / 60%);
}
.docs-links {
margin-top: 28px;
padding-bottom: 20px;
text-align: center;
font-size: 15px;
color: #999;
}
.docs-link {
color: #45aa89;
text-decoration: none;
}
.docs-link:hover {
color: #63e2b7;
}
</style>
+24 -9
View File
@@ -24,7 +24,8 @@ const message = useMessage()
const username = ref('')
const email = ref('')
const password = ref('')
const approval = ref(false)
const acceptTerms = ref(false)
const acceptPrivacy = ref(false)
const options = computed(() => {
return ['@mail.ru', '@yandex.ru', '@gmail.com'].map((suffix) => {
@@ -48,15 +49,22 @@ async function signin() {
}
async function signup() {
if (!approval.value) {
if (!acceptTerms.value || !acceptPrivacy.value) {
return
}
const res = await authStore.signup(username.value, email.value)
const res = await authStore.signup(
username.value,
email.value,
acceptTerms.value,
acceptPrivacy.value,
)
if (!res && authStore.error != null) {
message.error(authStore.error)
return
}
message.info('Пароль отправлен на почту')
acceptTerms.value = false
acceptPrivacy.value = false
}
async function sendNewPassword() {
@@ -101,16 +109,23 @@ async function sendNewPassword() {
autocomplete: 'disabled',
}" :options="options" placeholder="detective@mail.ru" clearable />
<div class="form-label">
<n-checkbox v-model:checked="approval">
Я согласен с
<a href="/user-agreement" target="_blank" class="docs-link">пользовательским соглашением</a><br />
и
<a href="/privacy-policy" target="_blank" class="docs-link">соглашением о персональных данных</a>
<n-checkbox v-model:checked="acceptTerms">
Я принимаю условия
<a href="/user-agreement" target="_blank" class="docs-link">пользовательского соглашения</a>
</n-checkbox>
</div>
<div class="form-label">
<n-checkbox v-model:checked="acceptPrivacy">
Я даю согласие на обработку
<a href="/privacy-policy" target="_blank" class="docs-link">персональных данных</a>
</n-checkbox>
</div>
<div class="form-button-wrapper">
<div class="form-label">
<n-button @click="signup" :disabled="username.length == 0 || password.length == 0 || !approval">
<n-button
@click="signup"
:disabled="username.length == 0 || email.length == 0 || !acceptTerms || !acceptPrivacy"
>
<span v-if="!authStore.isLoading"> Регистрация </span>
<span v-else> Подождите... </span>
</n-button>
+104 -2
View File
@@ -1,12 +1,15 @@
<script setup lang="ts">
import { NAlert } from 'naive-ui'
import { NAlert, NButton, NInput, NModal, useMessage } from 'naive-ui'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'
import HeaderMenu from '@/components/HeaderMenu.vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const router = useRouter()
const message = useMessage()
const client = getAuthClient()
const permissions = ref<string[]>([])
@@ -208,6 +211,22 @@ function getDetectiveTip(): Tip {
const tip = getDetectiveTip()
// ===== Удаление аккаунта =====
const showDeleteDialog = ref(false)
const deletePassword = ref('')
const deleteError = ref('')
async function confirmDeleteAccount() {
deleteError.value = ''
const res = await authStore.deleteAccount(deletePassword.value)
if (!res) {
deleteError.value = authStore.error || 'Не удалось удалить аккаунт'
return
}
message.success('Аккаунт и персональные данные удалены')
showDeleteDialog.value = false
router.push('/login')
}
</script>
<template>
@@ -218,10 +237,93 @@ const tip = getDetectiveTip()
</n-alert>
<p>Доброго времени суток, {{ authStore.username }}.</p>
<div class="danger-zone">
<div class="danger-zone-text">
<div class="danger-zone-title">Удаление аккаунта</div>
<div class="danger-zone-hint">
Удаление необратимо: аккаунт и все персональные данные будут стёрты (ст. 21 152-ФЗ).
</div>
</div>
<n-button type="error" ghost @click="showDeleteDialog = true">Удалить аккаунт</n-button>
</div>
</div>
</div>
<n-modal
v-model:show="showDeleteDialog"
preset="card"
title="Удаление аккаунта"
class="delete-modal"
>
<p class="delete-modal-text">
Это действие необратимо. Для подтверждения удаления введите пароль от учётной записи
он понадобится, чтобы убедиться, что аккаунт удаляет его владелец.
</p>
<n-input
v-model:value="deletePassword"
type="password"
placeholder="Пароль для подтверждения"
@keyup.enter="confirmDeleteAccount"
/>
<p v-if="deleteError" class="delete-modal-error">{{ deleteError }}</p>
<div class="delete-modal-buttons">
<n-button @click="showDeleteDialog = false">Отмена</n-button>
<n-button
type="error"
:disabled="deletePassword.length == 0"
:loading="authStore.isLoading"
@click="confirmDeleteAccount"
>
Удалить навсегда
</n-button>
</div>
</n-modal>
<HeaderMenu active="office" />
</template>
<style scoped></style>
<style scoped>
.danger-zone {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 40px;
padding: 20px;
background-color: #222;
border: 1px solid rgb(224 108 117 / 40%);
border-radius: 10px;
}
.danger-zone-title {
font-size: 17px;
font-weight: 600;
color: #e06c75;
}
.danger-zone-hint {
margin-top: 6px;
font-size: 14px;
color: #999;
}
.delete-modal-text {
margin: 0 0 16px;
line-height: 1.5;
color: #bbb;
}
.delete-modal-error {
margin: 12px 0 0;
color: #e06c75;
}
.delete-modal-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
</style>
+6
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import CatalogView from '../views/CatalogView.vue'
import ConsentView from '../views/ConsentView.vue'
import GamesView from '../views/GamesView.vue'
import GameView from '../views/GameView.vue'
import HomeView from '../views/HomeView.vue'
@@ -42,6 +43,11 @@ const router = createRouter({
name: 'privacy-policy',
component: PrivacyPolicyView,
},
{
path: '/consent',
name: 'consent',
component: ConsentView,
},
{
path: '/games',
name: 'games',
+33 -2
View File
@@ -132,13 +132,18 @@ export const useAuthStore = defineStore('auth', () => {
}
// Регистрация
async function signup(username: string, email: string) {
async function signup(
username: string,
email: string,
acceptTerms: boolean,
acceptPrivacy: boolean,
) {
isLoading.value = true
error.value = null
try {
const client = buildClient(getToken)
const response = await client.Signup({ username, email })
const response = await client.Signup({ username, email, acceptTerms, acceptPrivacy })
if (response.error) {
error.value = response.error
@@ -154,6 +159,31 @@ export const useAuthStore = defineStore('auth', () => {
}
}
// Удаление аккаунта (подтверждение владельца паролем, ст. 21 152-ФЗ)
async function deleteAccount(password: string) {
isLoading.value = true
error.value = null
try {
// Пароль передаётся HTTP-заголовком X-Password, а не в URL или body
const client = buildClient(getToken, () => ({ 'X-Password': password }))
const response = await client.DeleteAccount({})
if (response.error) {
error.value = response.error
return false
}
await logout()
return true
} catch (err) {
error.value = err instanceof Error ? err.message : 'Account deletion failed'
return false
} finally {
isLoading.value = false
}
}
// Восстановление пароля
async function refreshPassword(email: string) {
isLoading.value = true
@@ -216,6 +246,7 @@ export const useAuthStore = defineStore('auth', () => {
getToken,
login,
signup,
deleteAccount,
refreshPassword,
logout,
fetchUser,
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
import DocsPage from '@/components/DocsPage.vue'
import HeaderMenu from '@/components/HeaderMenu.vue'
</script>
<template>
<DocsPage kind="consent" />
<HeaderMenu />
</template>
+9 -2
View File
@@ -1,3 +1,10 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import DocsPage from '@/components/DocsPage.vue'
import HeaderMenu from '@/components/HeaderMenu.vue'
</script>
<template>Соглашением о персональных данных</template>
<template>
<DocsPage kind="privacy" />
<HeaderMenu />
</template>
+9 -2
View File
@@ -1,3 +1,10 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import DocsPage from '@/components/DocsPage.vue'
import HeaderMenu from '@/components/HeaderMenu.vue'
</script>
<template>Пользовательское соглашение</template>
<template>
<DocsPage kind="terms" />
<HeaderMenu />
</template>