Compare commits

...

3 Commits

Author SHA1 Message Date
VLADIMIR 606d515ade add buttons 2026-08-20 23:39:56 +07:00
VLADIMIR 4174ad304a update api 2026-08-20 23:22:46 +07:00
VLADIMIR 862c918981 updates 2026-08-20 01:57:39 +07:00
18 changed files with 1186 additions and 288 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"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вечерний детектив</title> <title>Вечерний детектив</title>
<script type="module" crossorigin src="/assets/index-B54R2K0c.js"></script> <script type="module" crossorigin src="/assets/index-CNblC4ZU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BdDl0UCc.css"> <link rel="stylesheet" crossorigin href="/assets/index-BlFsLzpe.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+106
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) { rpc GetUsers(GetUsersReq) returns (GetUsersRsp) {
option (google.api.http) = { option (google.api.http) = {
get: "/api/users" get: "/api/users"
@@ -324,6 +365,27 @@ service EveningDetectiveServer {
}; };
} }
rpc DownloadScenarioArchive(DownloadScenarioArchiveReq) returns (google.api.HttpBody) {
option (google.api.http) = {
get: "/api/scenarios/{id}/archive"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Сценарии";
summary: "Скачать сценарий архивом (со всеми материалами и картинками)";
};
}
rpc UploadScenarioArchive(google.api.HttpBody) returns (UploadScenarioArchiveRsp) {
option (google.api.http) = {
post: "/api/scenarios/archive"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Сценарии";
summary: "Создать сценарий из архива";
};
}
rpc AddGame(AddGameReq) returns (AddGameRsp) { rpc AddGame(AddGameReq) returns (AddGameRsp) {
option (google.api.http) = { option (google.api.http) = {
post: "/api/games" post: "/api/games"
@@ -527,6 +589,8 @@ 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 +626,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 {
@@ -771,6 +868,15 @@ message DeleteScenarioPlaceRsp {
string error = 1; string error = 1;
} }
message DownloadScenarioArchiveReq {
int32 id = 1;
}
message UploadScenarioArchiveRsp {
string error = 1;
int32 id = 2;
}
message AddGameReq { message AddGameReq {
string name = 1; string name = 1;
string description = 2; string description = 2;
+103
View File
@@ -0,0 +1,103 @@
import { useAuthStore } from '@/stores/auth'
import { getURL } from './generated/crabs/evening_detective_server/client'
export type DownloadArchiveResult = {
blob: Blob
filename: string
}
export type UploadArchiveResult = {
id: number | null
error: string | null
}
// Клиент для бинарных операций со сценарием (ZIP-архив). Сгенерированный
// HTTP-хендлер всегда парсит ответ как JSON, поэтому для архивов используем
// сырой fetch: скачивание отдаёт байты архива, загрузка принимает их напрямую.
export function getArchiveClient() {
const authStore = useAuthStore()
return buildArchiveClient(authStore.refreshTokenAction, authStore.getToken)
}
function buildArchiveClient(
refreshTokens: () => Promise<boolean>,
getToken: () => string | null,
) {
async function request(path: string, init: RequestInit): Promise<Response> {
const headers: Record<string, string> = {}
const token = getToken()
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
let response = await fetch(`${getURL()}/${path}`, {
...init,
headers: { ...headers, ...(init.headers as Record<string, string>) },
})
// Токен протух — обновляем и повторяем запрос один раз.
if (response.status == 401) {
await refreshTokens()
const newToken = getToken()
if (newToken) {
headers['Authorization'] = `Bearer ${newToken}`
}
response = await fetch(`${getURL()}/${path}`, {
...init,
headers: { ...headers, ...(init.headers as Record<string, string>) },
})
}
return response
}
async function downloadScenarioArchive(id: number): Promise<DownloadArchiveResult> {
const response = await request(`api/scenarios/${id}/archive`, { method: 'GET' })
if (!response.ok) {
throw new Error(await gatewayErrorMessage(response))
}
const blob = await response.blob()
const filename =
parseContentDispositionFilename(response.headers.get('Content-Disposition')) || `scenario-${id}.zip`
return { blob, filename }
}
async function uploadScenarioArchive(file: File): Promise<UploadArchiveResult> {
const contentType = file.type || 'application/zip'
const response = await request('api/scenarios/archive', {
method: 'POST',
headers: { 'Content-Type': contentType },
body: file,
})
const data = await response.json().catch(() => null)
if (!response.ok) {
return { id: null, error: data?.message || `Ошибка сервера (${response.status})` }
}
const error = typeof data?.error === 'string' && data.error !== '' ? data.error : null
return { id: typeof data?.id === 'number' ? data.id : null, error }
}
return { downloadScenarioArchive, uploadScenarioArchive }
}
// Извлекает имя файла из Content-Disposition: attachment; filename="name.zip"
function parseContentDispositionFilename(value: string | null): string {
if (!value) {
return ''
}
const match = value.match(/filename="?([^";]+)"?/i)
const filename = match ? match[1] : ''
return filename.replace(/[\\/:*?"<>|]/g, '_')
}
// Ошибки grpc-gateway приходят JSON-объектом {"code": ..., "message": ...}
async function gatewayErrorMessage(response: Response): Promise<string> {
const data = await response.json().catch(() => null)
if (data && typeof data.message === 'string' && data.message !== '') {
return data.message
}
return `Ошибка сервера (${response.status})`
}
@@ -1,11 +1,17 @@
import { createEveningDetectiveServerClient, type EveningDetectiveServer } from '.' import { createEveningDetectiveServerClient, type EveningDetectiveServer } from '.'
export function buildClient(getToken?: () => string | null): EveningDetectiveServer { export type ExtraHeadersProvider = () => Record<string, string> | null | undefined
return createEveningDetectiveServerClient(createHttpHandler(getToken))
export function buildClient(
getToken?: () => string | null,
getExtraHeaders?: ExtraHeadersProvider,
): EveningDetectiveServer {
return createEveningDetectiveServerClient(createHttpHandler(getToken, getExtraHeaders))
} }
export function createHttpHandler( export function createHttpHandler(
getToken?: () => string | null, getToken?: () => string | null,
getExtraHeaders?: ExtraHeadersProvider,
): (request: { path: string; method: string; body: string | null }) => Promise<unknown> { ): (request: { path: string; method: string; body: string | null }) => Promise<unknown> {
return async (request: { path: string; method: string; body: string | null }) => { return async (request: { path: string; method: string; body: string | null }) => {
const baseURL = getURL() const baseURL = getURL()
@@ -24,6 +30,12 @@ export function createHttpHandler(
headers['Authorization'] = `Bearer ${token}` headers['Authorization'] = `Bearer ${token}`
} }
// Добавляем дополнительные заголовки (например, X-Password для удаления аккаунта)
const extraHeaders = getExtraHeaders?.()
if (extraHeaders) {
Object.assign(headers, extraHeaders)
}
// Делаем запрос // Делаем запрос
const response = await fetch(url, { const response = await fetch(url, {
method: request.method, method: request.method,
@@ -1,5 +1,5 @@
// Code generated by protoc-gen-typescript-http. DO NOT EDIT. // Code generated by protoc-gen-typescript-http. DO NOT EDIT.
/* eslint-disable camelcase */
// @ts-nocheck // @ts-nocheck
export type PingReq = { export type PingReq = {
@@ -19,6 +19,8 @@ export type EchoRsp = {
export type SignupReq = { export type SignupReq = {
username: string | undefined; username: string | undefined;
email: string | undefined; email: string | undefined;
acceptTerms: boolean | undefined;
acceptPrivacy: boolean | undefined;
}; };
export type SignupRsp = { export type SignupRsp = {
@@ -54,6 +56,43 @@ export type RefreshRsp = {
refreshToken: string | undefined; 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 = { export type GetUsersReq = {
}; };
@@ -273,6 +312,15 @@ export type DeleteScenarioPlaceRsp = {
error: string | undefined; error: string | undefined;
}; };
export type DownloadScenarioArchiveReq = {
id: number | undefined;
};
export type UploadScenarioArchiveRsp = {
error: string | undefined;
id: number | undefined;
};
export type AddGameReq = { export type AddGameReq = {
name: string | undefined; name: string | undefined;
description: string | undefined; description: string | undefined;
@@ -456,6 +504,10 @@ export interface EveningDetectiveServer {
RefreshPassword(request: RefreshPasswordReq): Promise<RefreshPasswordRsp>; RefreshPassword(request: RefreshPasswordReq): Promise<RefreshPasswordRsp>;
Login(request: LoginReq): Promise<LoginRsp>; Login(request: LoginReq): Promise<LoginRsp>;
Refresh(request: RefreshReq): Promise<RefreshRsp>; 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>; GetUsers(request: GetUsersReq): Promise<GetUsersRsp>;
GetUserById(request: GetUserByIdReq): Promise<GetUserByIdRsp>; GetUserById(request: GetUserByIdReq): Promise<GetUserByIdRsp>;
GetMe(request: GetMeReq): Promise<GetMeRsp>; GetMe(request: GetMeReq): Promise<GetMeRsp>;
@@ -476,6 +528,8 @@ export interface EveningDetectiveServer {
AddScenarioPlace(request: AddScenarioPlaceReq): Promise<AddScenarioPlaceRsp>; AddScenarioPlace(request: AddScenarioPlaceReq): Promise<AddScenarioPlaceRsp>;
UpdateScenarioPlace(request: UpdateScenarioPlaceReq): Promise<UpdateScenarioPlaceRsp>; UpdateScenarioPlace(request: UpdateScenarioPlaceReq): Promise<UpdateScenarioPlaceRsp>;
DeleteScenarioPlace(request: DeleteScenarioPlaceReq): Promise<DeleteScenarioPlaceRsp>; DeleteScenarioPlace(request: DeleteScenarioPlaceReq): Promise<DeleteScenarioPlaceRsp>;
DownloadScenarioArchive(request: DownloadScenarioArchiveReq): Promise<googleapi_HttpBody>;
UploadScenarioArchive(request: googleapi_HttpBody): Promise<UploadScenarioArchiveRsp>;
AddGame(request: AddGameReq): Promise<AddGameRsp>; AddGame(request: AddGameReq): Promise<AddGameRsp>;
GetGames(request: GetGamesReq): Promise<GetGamesRsp>; GetGames(request: GetGamesReq): Promise<GetGamesRsp>;
GetGame(request: GetGameReq): Promise<GetGameRsp>; GetGame(request: GetGameReq): Promise<GetGameRsp>;
@@ -507,8 +561,8 @@ export function createEveningDetectiveServerClient(
handler: RequestHandler handler: RequestHandler
): EveningDetectiveServer { ): EveningDetectiveServer {
return { return {
Ping(request) { Ping(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/test/ping`; const path = `api/test/ping`; // eslint-disable-line quotes
const body = null; const body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -524,8 +578,8 @@ export function createEveningDetectiveServerClient(
method: "Ping", method: "Ping",
}) as Promise<PingRsp>; }) as Promise<PingRsp>;
}, },
Echo(request) { Echo(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/test/echo`; const path = `api/test/echo`; // eslint-disable-line quotes
const body = null; const body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
if (request.text) { if (request.text) {
@@ -544,8 +598,8 @@ export function createEveningDetectiveServerClient(
method: "Echo", method: "Echo",
}) as Promise<EchoRsp>; }) as Promise<EchoRsp>;
}, },
Signup(request) { Signup(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/signup`; const path = `api/auth/signup`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -561,8 +615,8 @@ export function createEveningDetectiveServerClient(
method: "Signup", method: "Signup",
}) as Promise<SignupRsp>; }) as Promise<SignupRsp>;
}, },
RefreshPassword(request) { RefreshPassword(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/refresh-password`; const path = `api/auth/refresh-password`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -578,8 +632,8 @@ export function createEveningDetectiveServerClient(
method: "RefreshPassword", method: "RefreshPassword",
}) as Promise<RefreshPasswordRsp>; }) as Promise<RefreshPasswordRsp>;
}, },
Login(request) { Login(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/login`; const path = `api/auth/login`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -595,8 +649,8 @@ export function createEveningDetectiveServerClient(
method: "Login", method: "Login",
}) as Promise<LoginRsp>; }) as Promise<LoginRsp>;
}, },
Refresh(request) { Refresh(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/auth/refresh`; const path = `api/auth/refresh`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -612,8 +666,76 @@ export function createEveningDetectiveServerClient(
method: "Refresh", method: "Refresh",
}) as Promise<RefreshRsp>; }) as Promise<RefreshRsp>;
}, },
GetUsers(request) { DeleteAccount(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/users`; 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -629,11 +751,11 @@ export function createEveningDetectiveServerClient(
method: "GetUsers", method: "GetUsers",
}) as Promise<GetUsersRsp>; }) as Promise<GetUsersRsp>;
}, },
GetUserById(request) { GetUserById(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -649,8 +771,8 @@ export function createEveningDetectiveServerClient(
method: "GetUserById", method: "GetUserById",
}) as Promise<GetUserByIdRsp>; }) as Promise<GetUserByIdRsp>;
}, },
GetMe(request) { GetMe(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/users/me`; const path = `api/users/me`; // eslint-disable-line quotes
const body = null; const body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -666,11 +788,11 @@ export function createEveningDetectiveServerClient(
method: "GetMe", method: "GetMe",
}) as Promise<GetMeRsp>; }) as Promise<GetMeRsp>;
}, },
AddUserRole(request) { AddUserRole(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -686,11 +808,11 @@ export function createEveningDetectiveServerClient(
method: "AddUserRole", method: "AddUserRole",
}) as Promise<AddUserRoleRsp>; }) as Promise<AddUserRoleRsp>;
}, },
DeleteUserRole(request) { DeleteUserRole(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -706,8 +828,8 @@ export function createEveningDetectiveServerClient(
method: "DeleteUserRole", method: "DeleteUserRole",
}) as Promise<DeleteUserRoleRsp>; }) as Promise<DeleteUserRoleRsp>;
}, },
GetPermissions(request) { GetPermissions(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/ui/permissions`; const path = `api/ui/permissions`; // eslint-disable-line quotes
const body = null; const body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -723,8 +845,8 @@ export function createEveningDetectiveServerClient(
method: "GetPermissions", method: "GetPermissions",
}) as Promise<GetPermissionsRsp>; }) as Promise<GetPermissionsRsp>;
}, },
UploadFile(request) { UploadFile(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/files/upload`; const path = `api/files/upload`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -740,11 +862,11 @@ export function createEveningDetectiveServerClient(
method: "UploadFile", method: "UploadFile",
}) as Promise<UploadFileRsp>; }) as Promise<UploadFileRsp>;
}, },
DownloadFile(request) { DownloadFile(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.filename) { if (!request.filename) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -760,8 +882,8 @@ export function createEveningDetectiveServerClient(
method: "DownloadFile", method: "DownloadFile",
}) as Promise<googleapi_HttpBody>; }) as Promise<googleapi_HttpBody>;
}, },
AddScenario(request) { AddScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/scenario`; const path = `api/scenario`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -777,8 +899,8 @@ export function createEveningDetectiveServerClient(
method: "AddScenario", method: "AddScenario",
}) as Promise<AddScenarioRsp>; }) as Promise<AddScenarioRsp>;
}, },
GetMyScenarios(request) { GetMyScenarios(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/my-scenarios`; const path = `api/my-scenarios`; // eslint-disable-line quotes
const body = null; const body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -794,8 +916,8 @@ export function createEveningDetectiveServerClient(
method: "GetMyScenarios", method: "GetMyScenarios",
}) as Promise<GetMyScenariosRsp>; }) as Promise<GetMyScenariosRsp>;
}, },
GetScenariosCatalog(request) { GetScenariosCatalog(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/catalog`; const path = `api/catalog`; // eslint-disable-line quotes
const body = null; const body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -811,11 +933,11 @@ export function createEveningDetectiveServerClient(
method: "GetScenariosCatalog", method: "GetScenariosCatalog",
}) as Promise<GetScenariosCatalogRsp>; }) as Promise<GetScenariosCatalogRsp>;
}, },
GetFullScenario(request) { GetFullScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -831,11 +953,11 @@ export function createEveningDetectiveServerClient(
method: "GetFullScenario", method: "GetFullScenario",
}) as Promise<GetScenarioRsp>; }) as Promise<GetScenarioRsp>;
}, },
GetScenario(request) { GetScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -851,11 +973,11 @@ export function createEveningDetectiveServerClient(
method: "GetScenario", method: "GetScenario",
}) as Promise<GetScenarioRsp>; }) as Promise<GetScenarioRsp>;
}, },
UpdateScenario(request) { UpdateScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -871,11 +993,11 @@ export function createEveningDetectiveServerClient(
method: "UpdateScenario", method: "UpdateScenario",
}) as Promise<UpdateScenarioRsp>; }) as Promise<UpdateScenarioRsp>;
}, },
PublicScenario(request) { PublicScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -891,11 +1013,11 @@ export function createEveningDetectiveServerClient(
method: "PublicScenario", method: "PublicScenario",
}) as Promise<PublicScenarioRsp>; }) as Promise<PublicScenarioRsp>;
}, },
DraftScenario(request) { DraftScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -911,11 +1033,11 @@ export function createEveningDetectiveServerClient(
method: "DraftScenario", method: "DraftScenario",
}) as Promise<DraftScenarioRsp>; }) as Promise<DraftScenarioRsp>;
}, },
DeleteScenario(request) { DeleteScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -931,11 +1053,11 @@ export function createEveningDetectiveServerClient(
method: "DeleteScenario", method: "DeleteScenario",
}) as Promise<DeleteScenarioRsp>; }) as Promise<DeleteScenarioRsp>;
}, },
AddScenarioPlace(request) { AddScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -951,14 +1073,14 @@ export function createEveningDetectiveServerClient(
method: "AddScenarioPlace", method: "AddScenarioPlace",
}) as Promise<AddScenarioPlaceRsp>; }) as Promise<AddScenarioPlaceRsp>;
}, },
UpdateScenarioPlace(request) { UpdateScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field request.id"); throw new Error("missing required field request.id");
} }
if (!request.code) { if (!request.code) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -974,14 +1096,14 @@ export function createEveningDetectiveServerClient(
method: "UpdateScenarioPlace", method: "UpdateScenarioPlace",
}) as Promise<UpdateScenarioPlaceRsp>; }) as Promise<UpdateScenarioPlaceRsp>;
}, },
DeleteScenarioPlace(request) { DeleteScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field request.id"); throw new Error("missing required field request.id");
} }
if (!request.code) { if (!request.code) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -997,8 +1119,45 @@ export function createEveningDetectiveServerClient(
method: "DeleteScenarioPlace", method: "DeleteScenarioPlace",
}) as Promise<DeleteScenarioPlaceRsp>; }) as Promise<DeleteScenarioPlaceRsp>;
}, },
AddGame(request) { DownloadScenarioArchive(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/games`; if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}/archive`; // 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: "DownloadScenarioArchive",
}) as Promise<googleapi_HttpBody>;
},
UploadScenarioArchive(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/scenarios/archive`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "POST",
body,
}, {
service: "EveningDetectiveServer",
method: "UploadScenarioArchive",
}) as Promise<UploadScenarioArchiveRsp>;
},
AddGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/games`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1014,8 +1173,8 @@ export function createEveningDetectiveServerClient(
method: "AddGame", method: "AddGame",
}) as Promise<AddGameRsp>; }) as Promise<AddGameRsp>;
}, },
GetGames(request) { GetGames(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/games`; const path = `api/games`; // eslint-disable-line quotes
const body = null; const body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1031,11 +1190,11 @@ export function createEveningDetectiveServerClient(
method: "GetGames", method: "GetGames",
}) as Promise<GetGamesRsp>; }) as Promise<GetGamesRsp>;
}, },
GetGame(request) { GetGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1051,11 +1210,11 @@ export function createEveningDetectiveServerClient(
method: "GetGame", method: "GetGame",
}) as Promise<GetGameRsp>; }) as Promise<GetGameRsp>;
}, },
UpdateGame(request) { UpdateGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1071,11 +1230,11 @@ export function createEveningDetectiveServerClient(
method: "UpdateGame", method: "UpdateGame",
}) as Promise<UpdateGameRsp>; }) as Promise<UpdateGameRsp>;
}, },
StartGame(request) { StartGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1091,11 +1250,11 @@ export function createEveningDetectiveServerClient(
method: "StartGame", method: "StartGame",
}) as Promise<StartGameRsp>; }) as Promise<StartGameRsp>;
}, },
PauseGame(request) { PauseGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1111,11 +1270,11 @@ export function createEveningDetectiveServerClient(
method: "PauseGame", method: "PauseGame",
}) as Promise<PauseGameRsp>; }) as Promise<PauseGameRsp>;
}, },
PlayGame(request) { PlayGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1131,11 +1290,11 @@ export function createEveningDetectiveServerClient(
method: "PlayGame", method: "PlayGame",
}) as Promise<PlayGameRsp>; }) as Promise<PlayGameRsp>;
}, },
FinishGame(request) { FinishGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1151,11 +1310,11 @@ export function createEveningDetectiveServerClient(
method: "FinishGame", method: "FinishGame",
}) as Promise<FinishGameRsp>; }) as Promise<FinishGameRsp>;
}, },
ResetGame(request) { ResetGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1171,11 +1330,11 @@ export function createEveningDetectiveServerClient(
method: "ResetGame", method: "ResetGame",
}) as Promise<ResetGameRsp>; }) as Promise<ResetGameRsp>;
}, },
DeleteGame(request) { DeleteGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1191,8 +1350,8 @@ export function createEveningDetectiveServerClient(
method: "DeleteGame", method: "DeleteGame",
}) as Promise<DeleteGameRsp>; }) as Promise<DeleteGameRsp>;
}, },
AddTeam(request) { AddTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/teams`; const path = `api/teams`; // eslint-disable-line quotes
const body = JSON.stringify(request); const body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1208,11 +1367,11 @@ export function createEveningDetectiveServerClient(
method: "AddTeam", method: "AddTeam",
}) as Promise<AddTeamRsp>; }) as Promise<AddTeamRsp>;
}, },
UpdateTeam(request) { UpdateTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1228,11 +1387,11 @@ export function createEveningDetectiveServerClient(
method: "UpdateTeam", method: "UpdateTeam",
}) as Promise<UpdateTeamRsp>; }) as Promise<UpdateTeamRsp>;
}, },
DeleteTeam(request) { DeleteTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1248,11 +1407,11 @@ export function createEveningDetectiveServerClient(
method: "DeleteTeam", method: "DeleteTeam",
}) as Promise<DeleteTeamRsp>; }) as Promise<DeleteTeamRsp>;
}, },
GiveTeamApplications(request) { GiveTeamApplications(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1268,11 +1427,11 @@ export function createEveningDetectiveServerClient(
method: "GiveTeamApplications", method: "GiveTeamApplications",
}) as Promise<GiveTeamApplicationsRsp>; }) as Promise<GiveTeamApplicationsRsp>;
}, },
GetTeamStory(request) { GetTeamStory(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
if (request.password) { if (request.password) {
@@ -1291,11 +1450,11 @@ export function createEveningDetectiveServerClient(
method: "GetTeamStory", method: "GetTeamStory",
}) as Promise<GetTeamStoryRsp>; }) as Promise<GetTeamStoryRsp>;
}, },
AddTeamAction(request) { AddTeamAction(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = JSON.stringify(request);
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; let uri = path;
@@ -1311,11 +1470,11 @@ export function createEveningDetectiveServerClient(
method: "AddTeamAction", method: "AddTeamAction",
}) as Promise<AddTeamActionRsp>; }) as Promise<AddTeamActionRsp>;
}, },
DeleteLastTeamAction(request) { DeleteLastTeamAction(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) { if (!request.id) {
throw new Error("missing required field 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 body = null;
const queryParams: string[] = []; const queryParams: string[] = [];
let uri = path; 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 username = ref('')
const email = ref('') const email = ref('')
const password = ref('') const password = ref('')
const approval = ref(false) const acceptTerms = ref(false)
const acceptPrivacy = ref(false)
const options = computed(() => { const options = computed(() => {
return ['@mail.ru', '@yandex.ru', '@gmail.com'].map((suffix) => { return ['@mail.ru', '@yandex.ru', '@gmail.com'].map((suffix) => {
@@ -48,15 +49,22 @@ async function signin() {
} }
async function signup() { async function signup() {
if (!approval.value) { if (!acceptTerms.value || !acceptPrivacy.value) {
return 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) { if (!res && authStore.error != null) {
message.error(authStore.error) message.error(authStore.error)
return return
} }
message.info('Пароль отправлен на почту') message.info('Пароль отправлен на почту')
acceptTerms.value = false
acceptPrivacy.value = false
} }
async function sendNewPassword() { async function sendNewPassword() {
@@ -101,16 +109,23 @@ async function sendNewPassword() {
autocomplete: 'disabled', autocomplete: 'disabled',
}" :options="options" placeholder="detective@mail.ru" clearable /> }" :options="options" placeholder="detective@mail.ru" clearable />
<div class="form-label"> <div class="form-label">
<n-checkbox v-model:checked="approval"> <n-checkbox v-model:checked="acceptTerms">
Я согласен с Я принимаю условия
<a href="/user-agreement" target="_blank" class="docs-link">пользовательским соглашением</a><br /> <a href="/user-agreement" target="_blank" class="docs-link">пользовательского соглашения</a>
и </n-checkbox>
<a href="/privacy-policy" target="_blank" class="docs-link">соглашением о персональных данных</a> </div>
<div class="form-label">
<n-checkbox v-model:checked="acceptPrivacy">
Я даю согласие на обработку
<a href="/privacy-policy" target="_blank" class="docs-link">персональных данных</a>
</n-checkbox> </n-checkbox>
</div> </div>
<div class="form-button-wrapper"> <div class="form-button-wrapper">
<div class="form-label"> <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-if="!authStore.isLoading"> Регистрация </span>
<span v-else> Подождите... </span> <span v-else> Подождите... </span>
</n-button> </n-button>
+104 -2
View File
@@ -1,12 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { NAlert } from 'naive-ui' import { NAlert, NButton, NInput, NModal, useMessage } from 'naive-ui'
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client' import { getAuthClient } from '@/api/auth_client'
import HeaderMenu from '@/components/HeaderMenu.vue' import HeaderMenu from '@/components/HeaderMenu.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter()
const message = useMessage()
const client = getAuthClient() const client = getAuthClient()
const permissions = ref<string[]>([]) const permissions = ref<string[]>([])
@@ -208,6 +211,22 @@ function getDetectiveTip(): Tip {
const tip = getDetectiveTip() 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> </script>
<template> <template>
@@ -218,10 +237,93 @@ const tip = getDetectiveTip()
</n-alert> </n-alert>
<p>Доброго времени суток, {{ authStore.username }}.</p> <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>
</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" /> <HeaderMenu active="office" />
</template> </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>
+38
View File
@@ -6,6 +6,7 @@ import { NAlert, NCard, NFlex, NInput, NModal, NSpace, NTag, NText, NUpload, NUp
import { ref } from 'vue' import { ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { getArchiveClient } from '@/api/archive_client'
import { getAuthClient } from '@/api/auth_client' import { getAuthClient } from '@/api/auth_client'
import type { Place, Scenario } from '@/api/generated/crabs/evening_detective_server' import type { Place, Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue' import HeaderMenu from '@/components/HeaderMenu.vue'
@@ -15,12 +16,14 @@ import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore() const authStore = useAuthStore()
const client = getAuthClient() const client = getAuthClient()
const archiveClient = getArchiveClient()
const route = useRoute() const route = useRoute()
const scenarioId = route.params.id const scenarioId = route.params.id
const showSettingsModal = ref(false) const showSettingsModal = ref(false)
const statusScenarioName = ref('') const statusScenarioName = ref('')
const deletedScenarioName = ref('') const deletedScenarioName = ref('')
const isDownloadingArchive = ref(false)
const message = useMessage() const message = useMessage()
@@ -105,6 +108,27 @@ async function draftScenario(id: number) {
showSettingsModal.value = false showSettingsModal.value = false
} }
async function downloadArchive() {
isDownloadingArchive.value = true
try {
const { blob, filename } = await archiveClient.downloadScenarioArchive(scenario.value.id!)
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
link.remove()
// Отзываем URL после того как браузер начал скачивание
setTimeout(() => URL.revokeObjectURL(url), 1000)
message.success('Архив сценария скачан')
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось скачать архив')
} finally {
isDownloadingArchive.value = false
}
}
async function uploadFile(file: File | null, filename: string): Promise<string> { async function uploadFile(file: File | null, filename: string): Promise<string> {
if (!file) { if (!file) {
return '' return ''
@@ -370,6 +394,15 @@ function getRandomComplimentForNastya() {
<n-button @click="updateScenario()" ghost> Сохранить изменения </n-button> <n-button @click="updateScenario()" ghost> Сохранить изменения </n-button>
<hr class="settings-hr" />
<p class="settings-header">Архив сценария</p>
<p class="settings-hint">
Скачайте сценарий ZIP-архивом (scenario.json + изображения) и импортируйте его в другом аккаунте.
</p>
<n-button :loading="isDownloadingArchive" :disabled="!scenario.id" ghost @click="downloadArchive()">
Скачать архив (.zip)
</n-button>
<hr class="settings-hr" /> <hr class="settings-hr" />
<p>Создать тестовую игру</p> <p>Создать тестовую игру</p>
<n-button @click="addGame(scenario.name || '', scenario.id!)" ghost> <n-button @click="addGame(scenario.name || '', scenario.id!)" ghost>
@@ -443,6 +476,11 @@ function getRandomComplimentForNastya() {
margin-top: 20px; margin-top: 20px;
} }
.settings-hint {
color: #999;
margin-bottom: 10px;
}
.settings-hr { .settings-hr {
margin: 20px 0; margin: 20px 0;
} }
+88 -1
View File
@@ -1,14 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { Archive } from '@vicons/carbon'
import { Icon } from '@vicons/utils'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { NButton, NCard, NFlex,NInput, NModal, NTag } from 'naive-ui' import { NButton, NCard, NFlex, NInput, NModal, NTag, NText, NUpload, NUploadDragger, type UploadFileInfo } from 'naive-ui'
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getArchiveClient } from '@/api/archive_client'
import { getAuthClient } from '@/api/auth_client' import { getAuthClient } from '@/api/auth_client'
import type { Scenario } from '@/api/generated/crabs/evening_detective_server' import type { Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue' import HeaderMenu from '@/components/HeaderMenu.vue'
const client = getAuthClient() const client = getAuthClient()
const archiveClient = getArchiveClient()
const scenarios = ref<Scenario[]>([]) const scenarios = ref<Scenario[]>([])
@@ -19,6 +23,10 @@ const message = useMessage()
const showAddScenarioModal = ref(false) const showAddScenarioModal = ref(false)
const newScenarioName = ref('') const newScenarioName = ref('')
const showImportModal = ref(false)
const importFileList = ref<UploadFileInfo[]>([])
const isImporting = ref(false)
async function getScenarios() { async function getScenarios() {
scenarios.value = [] scenarios.value = []
const res = await client.GetMyScenarios({}) const res = await client.GetMyScenarios({})
@@ -38,6 +46,33 @@ async function toScenarioEditor(id: number) {
router.push('/scenarios/' + id + '/editor') router.push('/scenarios/' + id + '/editor')
} }
function resetImport() {
importFileList.value = []
showImportModal.value = false
}
async function importScenario() {
const file = importFileList.value[0]?.file
if (!file) {
return
}
isImporting.value = true
try {
const res = await archiveClient.uploadScenarioArchive(file)
if (res.error) {
message.error(res.error)
return
}
message.success('Сценарий импортирован из архива')
resetImport()
await toScenarioEditor(res.id!)
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось импортировать архив')
} finally {
isImporting.value = false
}
}
getScenarios() getScenarios()
</script> </script>
@@ -51,6 +86,17 @@ getScenarios()
</div> </div>
</div> </div>
<div class="scenario-block" @click="showImportModal = true">
<div class="scenario-image-block scenario-image-plus import-icon">
<Icon>
<Archive />
</Icon>
</div>
<div class="scenario-content-block">
<p class="scenario-title">Импортировать из архива</p>
</div>
</div>
<div class="scenario-block" v-for="scenario in scenarios" @click="toScenarioEditor(scenario.id!)" <div class="scenario-block" v-for="scenario in scenarios" @click="toScenarioEditor(scenario.id!)"
v-bind:key="scenario.id"> v-bind:key="scenario.id">
<div class="scenario-image-block scenario-image" :style="{ backgroundImage: `url(${scenario.image})` }"></div> <div class="scenario-image-block scenario-image" :style="{ backgroundImage: `url(${scenario.image})` }"></div>
@@ -86,6 +132,37 @@ getScenarios()
</n-card> </n-card>
</n-modal> </n-modal>
<!-- Окно импорта сценария из архива -->
<n-modal v-model:show="showImportModal">
<n-card style="width: 600px" title="Импорт сценария из архива" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="resetImport()"> x </n-button>
</template>
<p class="import-hint">
Выберите ZIP-архив сценария (файл scenario.json + папка images). Архив можно получить
через «Скачать архив» в настройках сценария.
</p>
<n-upload :max="1" v-model:file-list="importFileList"
accept=".zip,application/zip,application/x-zip-compressed,application/octet-stream">
<n-upload-dragger>
<n-text style="font-size: 16px">
Щелкните или перетащите ZIP-архив в эту область
</n-text>
</n-upload-dragger>
</n-upload>
<template #footer>
<n-flex justify="end">
<n-button @click="resetImport()">Отмена</n-button>
<n-button type="primary" :loading="isImporting" :disabled="importFileList.length == 0"
@click="importScenario()">
Импортировать
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<HeaderMenu active="scenarios" /> <HeaderMenu active="scenarios" />
</template> </template>
@@ -126,6 +203,16 @@ getScenarios()
background-color: #111; background-color: #111;
} }
.import-icon svg {
height: 100px;
width: 100px;
}
.import-hint {
color: #aaa;
margin-bottom: 16px;
}
.scenario-image { .scenario-image {
background-size: cover; background-size: cover;
background-position: center; background-position: center;
+6
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import CatalogView from '../views/CatalogView.vue' import CatalogView from '../views/CatalogView.vue'
import ConsentView from '../views/ConsentView.vue'
import GamesView from '../views/GamesView.vue' import GamesView from '../views/GamesView.vue'
import GameView from '../views/GameView.vue' import GameView from '../views/GameView.vue'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
@@ -42,6 +43,11 @@ const router = createRouter({
name: 'privacy-policy', name: 'privacy-policy',
component: PrivacyPolicyView, component: PrivacyPolicyView,
}, },
{
path: '/consent',
name: 'consent',
component: ConsentView,
},
{ {
path: '/games', path: '/games',
name: '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 isLoading.value = true
error.value = null error.value = null
try { try {
const client = buildClient(getToken) const client = buildClient(getToken)
const response = await client.Signup({ username, email }) const response = await client.Signup({ username, email, acceptTerms, acceptPrivacy })
if (response.error) { if (response.error) {
error.value = 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) { async function refreshPassword(email: string) {
isLoading.value = true isLoading.value = true
@@ -216,6 +246,7 @@ export const useAuthStore = defineStore('auth', () => {
getToken, getToken,
login, login,
signup, signup,
deleteAccount,
refreshPassword, refreshPassword,
logout, logout,
fetchUser, 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>