Добавлено свойство flexWrap: "wrap" во все flex-контейнеры с кнопками, которые переполнялись на мобильных экранах. Теперь кнопки переносятся на новую строку вместо сжатия при ограниченной ширине окна. Исправленные места: - TwoFactorSetup: группа кнопок отключения (строка 310) и кнопки включения/отмены (строка 258) - LoginPage: кнопки верификации 2FA (строка 275) - InstallPrompt: кнопки установки/закрытия (строка 125) - GenerateTagsModal: кнопки выбрать все/снять все (строка 142) - NoteEditor: контейнер приватности заметки (строка 1084) - PublicProfilePage: строка аватара и информации профиля (строка 130) Каждая кнопка теперь имеет flex: "1 1 auto" с подходящим minWidth для сохранения пропорций при переносе. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
451 lines
16 KiB
TypeScript
451 lines
16 KiB
TypeScript
import React, { useState } from "react";
|
||
import { Icon } from "@iconify/react";
|
||
import { userApi } from "../../api/userApi";
|
||
import { useNotification } from "../../hooks/useNotification";
|
||
import { Modal } from "../common/Modal";
|
||
|
||
interface TwoFactorSetupProps {
|
||
twoFactorEnabled: boolean;
|
||
onStatusChange: () => void;
|
||
}
|
||
|
||
export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({
|
||
twoFactorEnabled,
|
||
onStatusChange,
|
||
}) => {
|
||
const { showNotification } = useNotification();
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [setupData, setSetupData] = useState<{
|
||
secret: string;
|
||
qrCode: string;
|
||
} | null>(null);
|
||
const [verificationCode, setVerificationCode] = useState("");
|
||
const [backupCodes, setBackupCodes] = useState<string[] | null>(null);
|
||
const [showDisableModal, setShowDisableModal] = useState(false);
|
||
const [disablePassword, setDisablePassword] = useState("");
|
||
const [isDisabling, setIsDisabling] = useState(false);
|
||
const [showBackupCodesModal, setShowBackupCodesModal] = useState(false);
|
||
const [isGeneratingBackupCodes, setIsGeneratingBackupCodes] = useState(false);
|
||
|
||
const handleSetup = async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
const data = await userApi.setup2FA();
|
||
setSetupData({
|
||
secret: data.secret,
|
||
qrCode: data.qrCode,
|
||
});
|
||
setVerificationCode("");
|
||
} catch (error: any) {
|
||
console.error("Ошибка настройки 2FA:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка настройки 2FA",
|
||
"error"
|
||
);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleEnable = async () => {
|
||
if (!setupData || !verificationCode.trim()) {
|
||
showNotification("Введите код подтверждения", "warning");
|
||
return;
|
||
}
|
||
|
||
if (verificationCode.trim().length !== 6) {
|
||
showNotification("Код должен содержать 6 цифр", "warning");
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
const response = await userApi.enable2FA(setupData.secret, verificationCode.trim());
|
||
setBackupCodes(response.backupCodes);
|
||
setShowBackupCodesModal(true);
|
||
setSetupData(null);
|
||
setVerificationCode("");
|
||
onStatusChange();
|
||
showNotification("2FA успешно включена", "success");
|
||
} catch (error: any) {
|
||
console.error("Ошибка включения 2FA:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка включения 2FA",
|
||
"error"
|
||
);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleDisable = async () => {
|
||
if (!disablePassword.trim()) {
|
||
showNotification("Введите пароль", "warning");
|
||
return;
|
||
}
|
||
|
||
setIsDisabling(true);
|
||
try {
|
||
await userApi.disable2FA(disablePassword);
|
||
setShowDisableModal(false);
|
||
setDisablePassword("");
|
||
onStatusChange();
|
||
showNotification("2FA успешно отключена", "success");
|
||
} catch (error: any) {
|
||
console.error("Ошибка отключения 2FA:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка отключения 2FA",
|
||
"error"
|
||
);
|
||
} finally {
|
||
setIsDisabling(false);
|
||
}
|
||
};
|
||
|
||
const handleGenerateBackupCodes = async () => {
|
||
setIsGeneratingBackupCodes(true);
|
||
try {
|
||
const response = await userApi.generateBackupCodes();
|
||
setBackupCodes(response.backupCodes);
|
||
setShowBackupCodesModal(true);
|
||
showNotification("Резервные коды успешно сгенерированы", "success");
|
||
} catch (error: any) {
|
||
console.error("Ошибка генерации резервных кодов:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка генерации резервных кодов",
|
||
"error"
|
||
);
|
||
} finally {
|
||
setIsGeneratingBackupCodes(false);
|
||
}
|
||
};
|
||
|
||
const copyToClipboard = (text: string) => {
|
||
navigator.clipboard.writeText(text);
|
||
showNotification("Скопировано в буфер обмена", "success");
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<h3>Двухфакторная аутентификация</h3>
|
||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "20px" }}>
|
||
Двухфакторная аутентификация добавляет дополнительный уровень
|
||
безопасности к вашему аккаунту. Используйте приложение-аутентификатор
|
||
(Google Authenticator, Microsoft Authenticator и т.д.) для генерации
|
||
кодов.
|
||
</p>
|
||
|
||
{!twoFactorEnabled ? (
|
||
<div>
|
||
{!setupData ? (
|
||
<div>
|
||
<button className="btnSave" onClick={handleSetup} disabled={isLoading}>
|
||
{isLoading ? (
|
||
<>
|
||
<Icon icon="mdi:loading" /> Настройка...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Icon icon="mdi:shield-lock" /> Включить 2FA
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<div
|
||
style={{
|
||
backgroundColor: "var(--card-bg)",
|
||
border: "1px solid var(--border-color)",
|
||
borderRadius: "8px",
|
||
padding: "20px",
|
||
marginBottom: "20px",
|
||
}}
|
||
>
|
||
<h4 style={{ marginTop: 0 }}>Шаг 1: Отсканируйте QR-код</h4>
|
||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "15px" }}>
|
||
Откройте приложение-аутентификатор на вашем телефоне и
|
||
отсканируйте этот QR-код:
|
||
</p>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "center",
|
||
marginBottom: "20px",
|
||
}}
|
||
>
|
||
<img
|
||
src={setupData.qrCode}
|
||
alt="QR Code"
|
||
style={{
|
||
width: "200px",
|
||
height: "200px",
|
||
border: "1px solid var(--border-color)",
|
||
borderRadius: "8px",
|
||
padding: "10px",
|
||
backgroundColor: "#fff",
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ marginBottom: "15px" }}>
|
||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "10px" }}>
|
||
Или введите секретный ключ вручную:
|
||
</p>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
gap: "10px",
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<code
|
||
style={{
|
||
flex: 1,
|
||
padding: "10px",
|
||
backgroundColor: "var(--bg-color)",
|
||
border: "1px solid var(--border-color)",
|
||
borderRadius: "4px",
|
||
fontSize: "14px",
|
||
wordBreak: "break-all",
|
||
}}
|
||
>
|
||
{setupData.secret}
|
||
</code>
|
||
<button
|
||
className="btnSave"
|
||
onClick={() => copyToClipboard(setupData.secret)}
|
||
style={{ padding: "10px 15px" }}
|
||
title="Копировать"
|
||
>
|
||
<Icon icon="mdi:content-copy" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
backgroundColor: "var(--card-bg)",
|
||
border: "1px solid var(--border-color)",
|
||
borderRadius: "8px",
|
||
padding: "20px",
|
||
marginBottom: "20px",
|
||
}}
|
||
>
|
||
<h4 style={{ marginTop: 0 }}>Шаг 2: Введите код подтверждения</h4>
|
||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "15px" }}>
|
||
Введите 6-значный код из приложения-аутентификатора:
|
||
</p>
|
||
<div className="form-group">
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
placeholder="000000"
|
||
value={verificationCode}
|
||
onChange={(e) => {
|
||
const value = e.target.value.replace(/\D/g, "").slice(0, 6);
|
||
setVerificationCode(value);
|
||
}}
|
||
maxLength={6}
|
||
style={{
|
||
textAlign: "center",
|
||
fontSize: "24px",
|
||
letterSpacing: "8px",
|
||
fontFamily: "monospace",
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
||
<button
|
||
className="btnSave"
|
||
onClick={handleEnable}
|
||
disabled={isLoading || verificationCode.length !== 6}
|
||
style={{ flex: "1 1 auto", minWidth: "120px" }}
|
||
>
|
||
{isLoading ? "Включение..." : "Включить 2FA"}
|
||
</button>
|
||
<button
|
||
className="btn-danger"
|
||
onClick={() => {
|
||
setSetupData(null);
|
||
setVerificationCode("");
|
||
}}
|
||
disabled={isLoading}
|
||
style={{ flex: "1 1 auto", minWidth: "100px" }}
|
||
>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<div
|
||
style={{
|
||
backgroundColor: "var(--card-bg)",
|
||
border: "1px solid #28a745",
|
||
borderRadius: "8px",
|
||
padding: "20px",
|
||
marginBottom: "20px",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: "10px",
|
||
marginBottom: "15px",
|
||
}}
|
||
>
|
||
<Icon icon="mdi:shield-check" style={{ fontSize: "24px", color: "#28a745" }} />
|
||
<span style={{ fontWeight: "600", color: "#28a745" }}>
|
||
2FA включена
|
||
</span>
|
||
</div>
|
||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "15px" }}>
|
||
Двухфакторная аутентификация активна для вашего аккаунта.
|
||
</p>
|
||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
||
<button
|
||
className="btnSave"
|
||
onClick={handleGenerateBackupCodes}
|
||
disabled={isGeneratingBackupCodes}
|
||
style={{ flex: "1 1 auto", minWidth: "200px" }}
|
||
>
|
||
{isGeneratingBackupCodes ? (
|
||
<>
|
||
<Icon icon="mdi:loading" /> Генерация...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Icon icon="mdi:key-variant" /> Сгенерировать резервные коды
|
||
</>
|
||
)}
|
||
</button>
|
||
<button
|
||
className="btn-danger"
|
||
onClick={() => setShowDisableModal(true)}
|
||
style={{ flex: "1 1 auto", minWidth: "140px" }}
|
||
>
|
||
<Icon icon="mdi:shield-off" /> Отключить 2FA
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Модальное окно для отключения 2FA */}
|
||
<Modal
|
||
isOpen={showDisableModal}
|
||
onClose={() => {
|
||
setShowDisableModal(false);
|
||
setDisablePassword("");
|
||
}}
|
||
onConfirm={handleDisable}
|
||
title="Отключение двухфакторной аутентификации"
|
||
message={
|
||
<>
|
||
<p style={{ marginBottom: "15px" }}>
|
||
Вы уверены, что хотите отключить двухфакторную аутентификацию? Это
|
||
снизит безопасность вашего аккаунта.
|
||
</p>
|
||
<div className="form-group">
|
||
<label htmlFor="disablePassword" style={{ marginBottom: "5px", display: "block" }}>
|
||
Введите пароль для подтверждения:
|
||
</label>
|
||
<input
|
||
type="password"
|
||
id="disablePassword"
|
||
className="form-input"
|
||
placeholder="Пароль"
|
||
value={disablePassword}
|
||
onChange={(e) => setDisablePassword(e.target.value)}
|
||
onKeyPress={(e) => {
|
||
if (e.key === "Enter" && !isDisabling && disablePassword.trim()) {
|
||
handleDisable();
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
</>
|
||
}
|
||
confirmText={isDisabling ? "Отключение..." : "Отключить"}
|
||
cancelText="Отмена"
|
||
confirmType="danger"
|
||
/>
|
||
|
||
{/* Модальное окно для показа резервных кодов */}
|
||
<Modal
|
||
isOpen={showBackupCodesModal}
|
||
onClose={() => setShowBackupCodesModal(false)}
|
||
onConfirm={() => setShowBackupCodesModal(false)}
|
||
title="Резервные коды"
|
||
message={
|
||
<>
|
||
<p
|
||
style={{
|
||
color: "#dc3545",
|
||
fontWeight: "bold",
|
||
marginBottom: "15px",
|
||
}}
|
||
>
|
||
⚠️ ВАЖНО: Сохраните эти коды в безопасном месте!
|
||
</p>
|
||
<p style={{ marginBottom: "15px" }}>
|
||
Если вы потеряете доступ к приложению-аутентификатору, вы сможете
|
||
использовать эти резервные коды для входа. Каждый код можно
|
||
использовать только один раз.
|
||
</p>
|
||
{backupCodes && (
|
||
<div
|
||
style={{
|
||
backgroundColor: "var(--bg-color)",
|
||
border: "1px solid var(--border-color)",
|
||
borderRadius: "8px",
|
||
padding: "15px",
|
||
marginBottom: "15px",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "repeat(2, 1fr)",
|
||
gap: "10px",
|
||
}}
|
||
>
|
||
{backupCodes.map((code, index) => (
|
||
<code
|
||
key={index}
|
||
style={{
|
||
padding: "8px",
|
||
backgroundColor: "var(--card-bg)",
|
||
border: "1px solid var(--border-color)",
|
||
borderRadius: "4px",
|
||
fontSize: "14px",
|
||
fontFamily: "monospace",
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
{code}
|
||
</code>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<p style={{ fontSize: "12px", color: "#666" }}>
|
||
Совет: Распечатайте эти коды или сохраните их в менеджере
|
||
паролей.
|
||
</p>
|
||
</>
|
||
}
|
||
confirmText="Я сохранил коды"
|
||
cancelText={null}
|
||
confirmType="primary"
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|