mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
274 lines
11 KiB
TypeScript
274 lines
11 KiB
TypeScript
import { useRef, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { motion, type Transition, type Variants } from "motion/react";
|
|
import { useImmer } from "use-immer";
|
|
import type { RegisterRequest } from "@/core/types";
|
|
import { useUserStore } from "@/state/user";
|
|
import { MaterialButton, MaterialIconButton } from "@/utils/material";
|
|
import api from "@/core/api";
|
|
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
|
import type { Alert, AlertType } from "./Auth";
|
|
import { AuthHeader, AlertsContainer } from "./Auth";
|
|
import styles from "./auth.module.scss";
|
|
|
|
const registerFieldVariants: Variants = {
|
|
initial: {
|
|
opacity: 0,
|
|
y: 10
|
|
},
|
|
animate: {
|
|
opacity: 1,
|
|
y: 0
|
|
}
|
|
};
|
|
|
|
const registerFieldTransition: Transition = {
|
|
duration: 0.3,
|
|
ease: "easeInOut"
|
|
};
|
|
|
|
const registerButtonVariants: Variants = {
|
|
initial: {
|
|
opacity: 0,
|
|
y: 10
|
|
},
|
|
animate: {
|
|
opacity: 1,
|
|
y: 0
|
|
}
|
|
};
|
|
|
|
const registerButtonTransition: Transition = {
|
|
duration: 0.3,
|
|
delay: 0.6,
|
|
ease: "easeInOut"
|
|
};
|
|
|
|
interface RegisterFormProps {
|
|
onSwitchMode: () => void;
|
|
}
|
|
|
|
export function RegisterForm({ onSwitchMode }: RegisterFormProps) {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
|
const setUser = useUserStore(state => state.setUser);
|
|
const navigate = useNavigate();
|
|
|
|
function showAlert(type: AlertType, message: string) {
|
|
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
|
}
|
|
|
|
const displayNameElement = useRef<AuthTextFieldHandle>(null);
|
|
const usernameElement = useRef<AuthTextFieldHandle>(null);
|
|
const passwordElement = useRef<AuthTextFieldHandle>(null);
|
|
const confirmPasswordElement = useRef<AuthTextFieldHandle>(null);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
|
|
if (isLoading) return;
|
|
|
|
const displayName = displayNameElement.current!.value.trim();
|
|
const username = usernameElement.current!.value.trim();
|
|
const password = passwordElement.current!.value.trim();
|
|
const confirmPassword = confirmPasswordElement.current!.value.trim();
|
|
|
|
if (!displayName || !username || !password || !confirmPassword) {
|
|
showAlert("danger", "Пожалуйста, заполните все поля");
|
|
return;
|
|
}
|
|
|
|
if (password !== confirmPassword) {
|
|
showAlert("danger", "Пароли не совпадают");
|
|
return;
|
|
}
|
|
|
|
if (displayName.length < 1 || displayName.length > 64) {
|
|
showAlert("danger", "Отображаемое имя должно быть от 1 до 64 символов");
|
|
return;
|
|
}
|
|
|
|
if (username.length < 3 || username.length > 20) {
|
|
showAlert("danger", "Имя пользователя должно быть от 3 до 20 символов");
|
|
return;
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
|
|
showAlert("danger", "Имя пользователя может содержать только английские буквы, цифры, дефисы и подчеркивания");
|
|
return;
|
|
}
|
|
|
|
if (password.length < 5 || password.length > 50) {
|
|
showAlert("danger", "Пароль должен быть от 5 до 50 символов");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const derived = await api.user.auth.deriveAuthSecret(username, password);
|
|
const request: RegisterRequest = {
|
|
display_name: displayName,
|
|
username: username,
|
|
password: derived,
|
|
confirm_password: derived
|
|
}
|
|
|
|
try {
|
|
const data = await api.user.auth.register(request);
|
|
setUser(data.token, data.user);
|
|
|
|
try {
|
|
await api.user.auth.ensureKeysOnLogin(password, data.token);
|
|
|
|
// Initialize Signal Protocol after keys are set up (non-blocking)
|
|
if (data.user?.id) {
|
|
// Run Signal Protocol initialization in background to avoid blocking navigation
|
|
(async () => {
|
|
try {
|
|
const { SignalProtocolService } = await import("@/utils/crypto/signalProtocol");
|
|
const { uploadPreKeyBundle, uploadAllPreKeys } = await import("@/core/api/crypto/prekeys");
|
|
const { uploadAllSessionsToServer, initializeSessionSync } = await import("@/utils/crypto/sessionSync");
|
|
|
|
const signalService = new SignalProtocolService(data.user!.id.toString());
|
|
await signalService.initialize();
|
|
|
|
// Initialize session sync (enables automatic upload of new sessions)
|
|
initializeSessionSync(data.user!.id.toString(), password, data.token);
|
|
|
|
// Upload base bundle with one prekey (for backward compatibility)
|
|
const bundle = await signalService.getPreKeyBundle();
|
|
await uploadPreKeyBundle(bundle, data.token);
|
|
|
|
// Upload all prekeys for server-side rotation
|
|
const baseBundle = await signalService.getBaseBundle();
|
|
const prekeys = await signalService.getAllPreKeys();
|
|
await uploadAllPreKeys(baseBundle, prekeys, data.token);
|
|
|
|
// Upload all current sessions to server (backup - will be empty on registration)
|
|
await uploadAllSessionsToServer(data.user!.id.toString(), password, data.token);
|
|
|
|
console.log(`Uploaded ${prekeys.length} prekeys to server`);
|
|
} catch (e) {
|
|
console.error("Key setup failed:", e);
|
|
}
|
|
})();
|
|
}
|
|
} catch (e) {
|
|
console.error("Key setup failed:", e);
|
|
}
|
|
|
|
navigate("/chat");
|
|
} catch (error: any) {
|
|
showAlert("danger", error.message || "Ошибка при регистрации");
|
|
}
|
|
} catch (error: any) {
|
|
showAlert("danger", error.message || "Ошибка соединения с сервером");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<AuthHeader
|
|
icon="person_add"
|
|
title="Регистрация"
|
|
subtitle="Создайте новый аккаунт"
|
|
/>
|
|
<div className={styles.authBody}>
|
|
<AlertsContainer alerts={alerts} />
|
|
<motion.form onSubmit={handleSubmit}>
|
|
<motion.div
|
|
initial="initial"
|
|
animate="animate"
|
|
variants={registerFieldVariants}
|
|
transition={registerFieldTransition}
|
|
>
|
|
<AuthTextField
|
|
label="Отображаемое имя"
|
|
name="display_name"
|
|
icon="badge--filled"
|
|
autocomplete="name"
|
|
maxlength={64}
|
|
counter
|
|
required
|
|
ref={displayNameElement} />
|
|
</motion.div>
|
|
<motion.div
|
|
initial="initial"
|
|
animate="animate"
|
|
variants={registerFieldVariants}
|
|
transition={registerFieldTransition}
|
|
>
|
|
<AuthTextField
|
|
label="@Имя пользователя"
|
|
name="username"
|
|
icon="person--filled"
|
|
autocomplete="username"
|
|
maxlength={20}
|
|
counter
|
|
required
|
|
ref={usernameElement} />
|
|
</motion.div>
|
|
<motion.div
|
|
initial="initial"
|
|
animate="animate"
|
|
variants={registerFieldVariants}
|
|
transition={registerFieldTransition}
|
|
>
|
|
<AuthTextField
|
|
label="Пароль"
|
|
name="password"
|
|
type="password"
|
|
toggle-password
|
|
icon="password--filled"
|
|
autocomplete="new-password"
|
|
required
|
|
ref={passwordElement} />
|
|
</motion.div>
|
|
<motion.div
|
|
initial="initial"
|
|
animate="animate"
|
|
variants={registerFieldVariants}
|
|
transition={registerFieldTransition}
|
|
>
|
|
<AuthTextField
|
|
label="Подтвердите пароль"
|
|
name="confirm_password"
|
|
type="password"
|
|
toggle-password
|
|
icon="password--filled"
|
|
autocomplete="new-password"
|
|
required
|
|
ref={confirmPasswordElement} />
|
|
</motion.div>
|
|
|
|
<div className={styles.authButtons}>
|
|
<motion.div
|
|
initial="initial"
|
|
animate="animate"
|
|
variants={registerButtonVariants}
|
|
transition={registerButtonTransition}
|
|
>
|
|
<MaterialIconButton icon="arrow_back" onClick={onSwitchMode} />
|
|
</motion.div>
|
|
<motion.div
|
|
initial="initial"
|
|
animate="animate"
|
|
variants={registerButtonVariants}
|
|
transition={registerButtonTransition}
|
|
>
|
|
<MaterialButton type="submit" disabled={isLoading} loading={isLoading} icon="person_add">
|
|
{isLoading ? "Регистрация..." : "Зарегистрироваться"}
|
|
</MaterialButton>
|
|
</motion.div>
|
|
</div>
|
|
|
|
</motion.form>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|