noteJS-react/src/components/twoFactor/TwoFactorSetup.tsx
root daa1c14d11 Исправлена мобильная адаптивность для кнопок в flex-контейнерах
Добавлено свойство 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>
2025-11-15 19:12:20 +07:00

451 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};