гітхаб + гугл + пошта з верифікацією (без прив'язки акаунтів)

翻译
import {
signInWithPopup,
signOut,
GithubAuthProvider,
GoogleAuthProvider,
onAuthStateChanged,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
sendEmailVerification,
sendPasswordResetEmail
} from "firebase/auth";
import { auth } from "./firebase";
const githubProvider = new GithubAuthProvider();
const googleProvider = new GoogleAuthProvider();
// логін через GitHub
export async function loginWithGitHub() {
try {
const result = await signInWithPopup(auth, githubProvider);
console.log("Успішний вхід через GitHub:", result.user);
return result.user;
} catch (error) {
console.error("Помилка входу через GitHub:", error);
throw error;
}
}
// логін через Google
export async function loginWithGoogle() {
try {
const result = await signInWithPopup(auth, googleProvider);
console.log("Успішний вхід через Google:", result.user);
return result.user;
} catch (error) {
console.error("Помилка входу через Google:", error);
throw error;
}
}
// реєстрація через email та пароль
export async function registerWithEmail(email, password) {
try {
const result = await createUserWithEmailAndPassword(
auth,
email,
password
);
await sendEmailVerification(result.user);
await signOut(auth);
console.log(
"Користувача зареєстровано. Лист для підтвердження email надіслано:",
result.user.email
);
return result.user;
} catch (error) {
console.error(
"Помилка реєстрації через email:",
error
);
throw error;
}
}
// логін через email та пароль
export async function loginWithEmail(email, password) {
try {
const result = await signInWithEmailAndPassword(
auth,
email,
password
);
if (!result.user.emailVerified) {
await signOut(auth);
const error = new Error(
"Email користувача не підтверджено."
);
error.code = "auth/email-not-verified";
throw error;
}
console.log(
"Успішний вхід через email:",
result.user
);
return result.user;
} catch (error) {
console.error(
"Помилка входу через email:",
error
);
throw error;
}
}
// повторне надсилання листа підтвердження
export async function resendEmailVerification(email, password) {
try {
const result = await signInWithEmailAndPassword(
auth,
email,
password
);
if (result.user.emailVerified) {
await signOut(auth);
return;
}
await sendEmailVerification(result.user);
await signOut(auth);
console.log(
"Лист для підтвердження email надіслано повторно:",
email
);
} catch (error) {
console.error(
"Помилка повторного надсилання листа:",
error
);
try {
await signOut(auth);
} catch {
// користувач уже вийшов із системи
}
throw error;
}
}
// відновлення пароля
export async function resetPassword(email) {
try {
await sendPasswordResetEmail(auth, email);
} catch (error) {
console.error(
"Помилка відновлення пароля:",
error
);
throw error;
}
}
// вихід
export async function logout() {
await signOut(auth);
}
// отримати Firebase ID Token для бекенду
export async function getIdToken(forceRefresh = false) {
const user = auth.currentUser;
if (!user) {
return null;
}
return await user.getIdToken(forceRefresh);
}
// підписка на зміну користувача
export function onUserChanged(callback) {
return onAuthStateChanged(auth, callback);
}
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: radial-gradient( 1200px 600px at 15% -10%, rgba(0, 77, 152, 0.35), transparent 60% ), radial-gradient( 1000px 500px at 110% 0%, rgba(230, 10, 62, 0.25), transparent 55% ), #050914;
}
.login-card {
width: 100%;
max-width: 420px;
background: linear-gradient( 180deg, #10182c 0%, rgba(16, 24, 44, 0.85) 100% );
border: 1px solid #23304a;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.5);
animation: card-in 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
@keyframes card-in {
from {
opacity: 0;
transform: translateY(20px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.login-logo {
text-align: center;
font-size: 3.2rem;
padding: 34px 32px 10px;
filter: drop-shadow( 0 6px 14px rgba(230, 10, 62, 0.5) );
}
.login-card h1 {
text-align: center;
font-family: 'Archivo Black', 'Inter', sans-serif;
font-size: 1.7rem;
letter-spacing: -0.5px;
color: #fff;
text-transform: uppercase;
margin: 0;
}
.login-description {
color: #8b96b3;
font-size: 0.92rem;
line-height: 1.5;
text-align: center;
margin: 10px 32px 28px;
}
.login-buttons {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 32px;
}
.login-btn {
width: 100%;
min-height: 52px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 14px 24px;
border-radius: 12px;
font-family: 'Inter', system-ui, sans-serif;
font-weight: 700;
font-size: 0.95rem;
cursor: pointer;
transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease, border-color 0.25s ease;
}
.login-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.login-provider-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1rem;
}
.login-btn.google {
background: #fff;
border: 1px solid #dadce0;
color: #202124;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.25);
}
.login-btn.google:hover:not(:disabled) {
background: #f8f9fa;
transform: translateY(-2px);
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.35);
}
.login-btn.google:active:not(:disabled) {
transform: translateY(0);
}
.login-btn.github {
background: #24292e;
border: 1px solid #3b4147;
color: #fff;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
}
.login-btn.github:hover:not(:disabled) {
background: #2f363d;
border-color: #4a5158;
transform: translateY(-2px);
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.45);
}
.login-btn.github:active:not(:disabled) {
transform: translateY(0);
}
.github-icon {
color: #fff;
}
.login-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 32px;
color: #5a6680;
font-size: 0.75rem;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: #23304a;
}
.login-divider span {
flex-shrink: 0;
}
.email-form {
padding: 0 32px;
}
.form-field {
margin-bottom: 16px;
}
.form-field label {
display: block;
margin-bottom: 7px;
color: #c2c9d8;
font-size: 0.8rem;
font-weight: 600;
text-align: left;
}
.form-field input {
width: 100%;
box-sizing: border-box;
min-height: 48px;
padding: 12px 14px;
border: 1px solid #303c56;
border-radius: 10px;
outline: none;
background: #0b1222;
color: #fff;
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.9rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.form-field input::placeholder {
color: #59657e;
}
.form-field input:focus {
border-color: #5379b8;
background: #0d1528;
box-shadow: 0 0 0 3px rgba(83, 121, 184, 0.12);
}
.form-field input:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.login-btn.email {
margin-top: 4px;
background: #e60a3e;
border: 1px solid #e60a3e;
color: #fff;
box-shadow: 0 10px 28px rgba(230, 10, 62, 0.22);
}
.login-btn.email:hover:not(:disabled) {
background: #f0184d;
border-color: #f0184d;
transform: translateY(-2px);
box-shadow: 0 14px 32px rgba(230, 10, 62, 0.3);
}
.login-btn.email:active:not(:disabled) {
transform: translateY(0);
}
.forgot-password {
display: block;
margin: 14px auto 0;
padding: 0;
border: none;
background: none;
color: #8b96b3;
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.78rem;
cursor: pointer;
}
.forgot-password:hover:not(:disabled) {
color: #c2c9d8;
}
.forgot-password:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.login-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 18px 32px 0;
color: #8b96b3;
font-size: 0.82rem;
}
.login-loading .spinner {
width: 18px;
height: 18px;
border-width: 2px;
}
.login-message {
margin: 16px 32px 0;
background: rgba(50, 190, 120, 0.1);
border: 1px solid rgba(50, 190, 120, 0.3);
color: #8ee0b3;
padding: 12px 16px;
border-radius: 10px;
font-size: 0.82rem;
font-weight: 500;
line-height: 1.45;
text-align: center;
}
.login-error {
margin: 16px 32px 0;
background: rgba(255, 77, 94, 0.12);
border: 1px solid rgba(255, 77, 94, 0.4);
color: #ff9aa5;
padding: 12px 16px;
border-radius: 10px;
font-size: 0.82rem;
font-weight: 500;
line-height: 1.45;
text-align: center;
}
.verification-button {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
width: calc(100% - 64px);
margin: 14px 32px 0;
padding: 11px 16px;
border: 1px solid rgba(83, 121, 184, 0.35);
border-radius: 10px;
background: rgba(83, 121, 184, 0.08);
color: #8faedc;
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.verification-button span {
color: #c2c9d8;
font-size: 0.72rem;
font-weight: 500;
}
.verification-button:hover:not(:disabled) {
background: rgba(83, 121, 184, 0.14);
border-color: rgba(83, 121, 184, 0.55);
color: #b5c9eb;
}
.verification-button:hover:not(:disabled) span {
color: #e0e5ef;
}
.verification-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.login-mode-switch {
display: block;
margin: 22px auto 0;
padding: 0;
border: none;
background: none;
color: #ffc72c;
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
}
.login-mode-switch:hover:not(:disabled) {
color: #ffd95f;
}
.login-mode-switch:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.login-footer {
margin: 28px 32px 30px;
padding-top: 18px;
border-top: 1px solid #23304a;
font-size: 0.75rem;
color: #5a6680;
letter-spacing: 0.3px;
text-align: center;
}
@media (max-width: 520px) {
.login-page {
padding: 16px;
}
.login-card {
max-width: 420px;
}
.login-logo {
padding-top: 28px;
}
.login-buttons,
.email-form {
padding-left: 24px;
padding-right: 24px;
}
.login-divider {
margin-left: 24px;
margin-right: 24px;
}
.login-message,
.login-error,
.login-loading {
margin-left: 24px;
margin-right: 24px;
}
.verification-button {
width: calc(100% - 48px);
margin-left: 24px;
margin-right: 24px;
}
.login-footer {
margin-left: 24px;
margin-right: 24px;
}
.login-description {
margin-left: 24px;
margin-right: 24px;
}
}
import { useState } from 'react';
import {
loginWithGitHub,
loginWithGoogle,
loginWithEmail,
registerWithEmail,
resendEmailVerification,
resetPassword
} from '../firebase/authService';
import './styles/Login.css';
export default function Login() {
const [isRegistering, setIsRegistering] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const clearMessages = () => {
setError('');
setMessage('');
};
const handleGoogleLogin = async () => {
setLoading(true);
clearMessages();
try {
await loginWithGoogle();
} catch (error) {
console.error(error);
if (error.code === 'auth/popup-closed-by-user') {
setError('Вікно входу було закрито.');
} else if (error.code === 'auth/popup-blocked') {
setError('Браузер заблокував спливаюче вікно.');
} else if (
error.code === 'auth/account-exists-with-different-credential'
) {
setError(
'Акаунт із таким email уже існує через інший спосіб входу.'
);
} else {
setError('Не вдалося виконати вхід через Google.');
}
} finally {
setLoading(false);
}
};
const handleGitHubLogin = async () => {
setLoading(true);
clearMessages();
try {
await loginWithGitHub();
} catch (error) {
console.error(error);
if (error.code === 'auth/popup-closed-by-user') {
setError('Вікно входу було закрито.');
} else if (error.code === 'auth/popup-blocked') {
setError('Браузер заблокував спливаюче вікно.');
} else if (
error.code === 'auth/account-exists-with-different-credential'
) {
setError(
'Акаунт із таким email уже існує через інший спосіб входу.'
);
} else {
setError('Не вдалося виконати вхід через GitHub.');
}
} finally {
setLoading(false);
}
};
const handleEmailSubmit = async (event) => {
event.preventDefault();
clearMessages();
if (!email.trim()) {
setError('Введіть email.');
return;
}
if (!password) {
setError('Введіть пароль.');
return;
}
setLoading(true);
try {
if (isRegistering) {
await registerWithEmail(email, password);
setMessage(
'Акаунт створено. Лист для підтвердження email надіслано на вашу електронну пошту. Перейдіть за посиланням у листі, а потім увійдіть.'
);
} else {
await loginWithEmail(email, password);
}
} catch (error) {
console.error(error);
if (error.code === 'auth/email-not-verified') {
setError(
'Email ще не підтверджено. Перевірте пошту та перейдіть за посиланням у листі.'
);
} else if (error.code === 'auth/email-already-in-use') {
setError(
'Користувач із таким email уже існує. Спробуйте увійти.'
);
} else if (error.code === 'auth/invalid-credential') {
setError('Неправильний email або пароль.');
} else if (error.code === 'auth/invalid-email') {
setError('Введіть коректну адресу електронної пошти.');
} else if (error.code === 'auth/weak-password') {
setError('Пароль має містити щонайменше 6 символів.');
} else if (error.code === 'auth/too-many-requests') {
setError('Занадто багато спроб. Спробуйте пізніше.');
} else if (error.code === 'auth/network-request-failed') {
setError(
'Помилка мережі. Перевірте підключення до інтернету.'
);
} else {
setError(
isRegistering
? 'Не вдалося створити акаунт.'
: 'Не вдалося виконати вхід.'
);
}
} finally {
setLoading(false);
}
};
const handleResendVerification = async () => {
clearMessages();
if (!email.trim()) {
setError('Введіть email.');
return;
}
if (!password) {
setError(
'Введіть пароль, щоб повторно надіслати лист підтвердження.'
);
return;
}
setLoading(true);
try {
await resendEmailVerification(email, password);
setMessage(
'Лист для підтвердження email надіслано повторно. Перевірте пошту та папку «Спам».'
);
} catch (error) {
console.error(error);
if (error.code === 'auth/invalid-credential') {
setError('Неправильний email або пароль.');
} else if (error.code === 'auth/invalid-email') {
setError('Введіть коректну адресу електронної пошти.');
} else if (error.code === 'auth/too-many-requests') {
setError('Занадто багато запитів. Спробуйте пізніше.');
} else if (error.code === 'auth/network-request-failed') {
setError(
'Помилка мережі. Перевірте підключення до інтернету.'
);
} else {
setError(
'Не вдалося повторно надіслати лист підтвердження.'
);
}
} finally {
setLoading(false);
}
};
const handlePasswordReset = async () => {
clearMessages();
if (!email.trim()) {
setError('Введіть email для відновлення пароля.');
return;
}
setLoading(true);
try {
await resetPassword(email);
setMessage(
'Лист для відновлення пароля надіслано на вашу електронну пошту.'
);
} catch (error) {
console.error(error);
if (error.code === 'auth/invalid-email') {
setError('Введіть коректну адресу електронної пошти.');
} else if (error.code === 'auth/too-many-requests') {
setError('Занадто багато запитів. Спробуйте пізніше.');
} else if (error.code === 'auth/network-request-failed') {
setError(
'Помилка мережі. Перевірте підключення до інтернету.'
);
} else {
setError(
'Не вдалося надіслати лист для відновлення пароля.'
);
}
} finally {
setLoading(false);
}
};
const toggleMode = () => {
setIsRegistering((value) => !value);
clearMessages();
};
return (
</div>

Футбольна ліга

Менеджер гравців та команд
</p>
<button
className="login-btn google"
onClick={handleGoogleLogin}
disabled={loading}
>
<svg
className="login-provider-icon"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="#4285F4"
d="M21.35 12.27c0-.79-.07-1.55-.23-2.27H12v4.3h5.22a4.46 4.46 0 0 1-1.94 2.93v2.44h3.14c1.84-1.69 2.93-4.18 2.93-7.4z"
/>
<path
fill="#34A853"
d="M12 21.7c2.63 0 4.84-.87 6.45-2.36l-3.14-2.44c-.87.58-1.98.92-3.31.92-2.54 0-4.69-1.72-5.46-4.03H3.3v2.52A9.74 9.74 0 0 0 12 21.7z"
/>
<path
fill="#FBBC05"
d="M6.54 13.79A5.85 5.85 0 0 1 6.24 12c0-.62.11-1.22.3-1.79V7.69H3.3A9.75 9.75 0 0 0 2.25 12c0 1.57.38 3.05 1.05 4.31l3.24-2.52z"
/>
<path
fill="#EA4335"
d="M12 6.18c1.43 0 2.72.49 3.73 1.45l2.8-2.8C16.83 3.15 14.63 2.3 12 2.3a9.74 9.74 0 0 0-8.7 5.39l3.24 2.52C7.31 7.9 9.46 6.18 12 6.18z"
/>
</svg>
Увійти через Google
</button>
<button
className="login-btn github"
onClick={handleGitHubLogin}
disabled={loading}
>
<svg
className="login-provider-icon github-icon"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="currentColor"
d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.57.1.78-.25.78-.55v-2.1c-3.2.7-3.88-1.54-3.88-1.54-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.67 1.24 3.32.95.1-.74.4-1.24.73-1.53-2.55-.29-5.23-1.28-5.23-5.68 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.47.11-3.06 0 0 .96-.31 3.15 1.18a10.9 10.9 0 0 1 5.73 0c2.19-1.49 3.15-1.18 3.15-1.18.62 1.59.23 2.77.11 3.06.73.81 1.18 1.84 1.18 3.1 0 4.41-2.69 5.39-5.25 5.67.41.36.78 1.08.78 2.18v3.23c0 .3.21.65.79.54A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5z"
/>
</svg>
Увійти через GitHub
</button>
</div>
або
</div>
<form
className="email-form"
onSubmit={handleEmailSubmit}
>
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(event) =>
setEmail(event.target.value)
}
placeholder="example@email.com"
autoComplete="email"
disabled={loading}
/>
</div>
Пароль
</label>
<input
id="password"
type="password"
value={password}
onChange={(event) =>
setPassword(event.target.value)
}
placeholder="Введіть пароль"
autoComplete={
isRegistering
? 'new-password'
: 'current-password'
}
disabled={loading}
/>
</div>
{!isRegistering && (
<button
type="button"
className="forgot-password"
onClick={handlePasswordReset}
disabled={loading}
>
Забули пароль?
</button>
)}
<button
type="submit"
className="login-btn email"
disabled={loading}
>
{isRegistering
? 'Зареєструватися'
: 'Увійти'}
</button>
</form>
{loading && (
Зачекайте...
</div>
)}
{error && (
{error}
</div>
)}
{message && (
{message}
</div>
)}
{!isRegistering && (
<button
type="button"
className="verification-button"
onClick={handleResendVerification}
disabled={loading}
>
Підтвердьте email
Надіслати лист повторно
</span>
</button>
)}
<button
type="button"
className="login-mode-switch"
onClick={toggleMode}
disabled={loading}
>
{isRegistering
? 'Вже маєте акаунт? Увійти'
: 'Ще не маєте акаунта? Зареєструватися'}
</button>
Авторизація через Firebase
</div>
</div>
</div>
);
}
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论