Добавлена система регистрации, небезопасная

This commit is contained in:
Georgiy Syralev
2025-09-14 23:19:09 +03:00
parent ca4d7abc18
commit 61bbeb3347
15 changed files with 499 additions and 142 deletions

View File

@@ -12,6 +12,7 @@
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"framer-motion": "^12.23.12",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.1",
@@ -8424,6 +8425,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.23.12",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
"integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.12",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -11524,6 +11552,21 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/motion-dom": {
"version": "12.23.12",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
"integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@@ -7,6 +7,7 @@
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"framer-motion": "^12.23.12",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.1",

View File

@@ -1,20 +1,37 @@
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Index from './pages/index';
import Pricing from './pages/pricing';
import Login from './pages/login';
import Dashboard from './pages/dashboard/index';
import './styles/tailwind.css';
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import Navbar from "./components/Navbar";
import Footer from "./components/Footer";
import Home from "./pages/index";
import Login from "./pages/login";
import Register from "./pages/register";
import Dashboard from "./pages/dashboard";
export default function App() {
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/pricing" element={<Pricing />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard/*" element={<Dashboard />} />
</Routes>
</BrowserRouter>
<AuthProvider>
<Router>
<div className="flex flex-col min-h-screen">
{/* Навигация сверху */}
<Navbar />
{/* Основной контент */}
<main className="flex-grow">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/dashboard/*" element={<Dashboard />} />
</Routes>
</main>
{/* Подвал */}
<Footer />
</div>
</Router>
</AuthProvider>
);
}
export default App;

View File

@@ -1,7 +1,9 @@
const Footer: React.FC = () => {
return (
<footer className="bg-gray-900 text-white p-4 text-center">
© {new Date().getFullYear()} osapab.host. Все права защищены.
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto text-center">
© 2025 ospab.host. Все права защищены.
</div>
</footer>
);
};

View File

@@ -1,30 +1,47 @@
import { Link } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
interface NavbarProps {
user: { email: string } | null;
logout: () => void;
}
export default function Navbar() {
const { user, logout } = useAuth();
const Navbar: React.FC<NavbarProps> = ({ user, logout }) => {
return (
<nav className="bg-gray-800 text-white p-4 flex justify-between items-center">
<div className="text-xl font-bold">ospab.host</div>
<div className="space-x-4">
<Link to="/">Главная</Link>
<Link to="/pricing">Цены</Link>
{user ? (
<>
<Link to="/dashboard">ЛК</Link>
<button onClick={logout} className="bg-red-600 px-2 py-1 rounded">
Выйти
</button>
</>
) : (
<Link to="/login">Вход</Link>
)}
<nav className="bg-white shadow-md px-8 py-4 fixed w-full z-50 rounded-b-3xl">
<div className="container mx-auto flex justify-between items-center">
<Link to="/" className="text-2xl font-bold text-indigo-700 hover:text-pink-500 transition">
ospab.host
</Link>
<div className="space-x-6">
<Link to="/" className="hover:text-pink-500 transition">Главная</Link>
<Link to="/pricing" className="hover:text-pink-500 transition">Цены</Link>
{user ? (
<>
<Link to="/dashboard" className="hover:text-pink-500 transition">Личный кабинет</Link>
<span className="text-gray-700 font-semibold">Привет, {user.name}</span>
<button
onClick={logout}
className="bg-red-500 text-white px-4 py-2 rounded-full hover:bg-pink-500 transition-all"
>
Выйти
</button>
</>
) : (
<>
<Link
to="/login"
className="bg-indigo-700 text-white px-4 py-2 rounded-full hover:bg-pink-500 transition-all"
>
Войти
</Link>
<Link
to="/register"
className="bg-white text-indigo-700 px-4 py-2 rounded-full border border-indigo-700 hover:bg-pink-500 hover:text-white transition-all"
>
Регистрация
</Link>
</>
)}
</div>
</div>
</nav>
);
};
export default Navbar;
}

View File

@@ -0,0 +1,48 @@
import React, { createContext, useContext, useState, ReactNode } from "react";
interface User {
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => boolean;
register: (name: string, email: string, password: string, password2: string) => boolean;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const login = (email: string, password: string) => {
if (email && password) {
setUser({ name: "Пользователь", email });
return true;
}
return false;
};
const register = (name: string, email: string, password: string, password2: string) => {
if (!name || !email || !password || !password2) return false;
if (password !== password2) return false;
setUser({ name, email });
return true;
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, register, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth must be used within AuthProvider");
return context;
};

View File

@@ -9,18 +9,13 @@ import './styles/tailwind.css';
const root = ReactDOM.createRoot(document.getElementById('root')!);
// Заглушка для функции login
const login = (email: string) => {
// Здесь может быть логика авторизации
console.log('Вход для:', email);
};
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/pricing" element={<Pricing />} />
<Route path="/login" element={<Login login={login} />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard/*" element={<Dashboard />} />
</Routes>
</BrowserRouter>

View File

@@ -1,5 +1,17 @@
import React from 'react';
import Navbar from "../../components/Navbar";
export default function Billing() {
return <div>Billing details</div>;
}
const Billing: React.FC = () => {
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<section className="pt-32 px-4 container mx-auto">
<h1 className="text-3xl font-bold mb-6">Баланс и платежи</h1>
<div className="bg-white rounded-3xl shadow p-6">
<p>Информация о платежах появится здесь.</p>
</div>
</section>
</div>
);
};
export default Billing;

View File

@@ -1,23 +1,34 @@
import { Link, Routes, Route } from "react-router-dom";
import Servers from "./servers";
import Billing from "./billing";
import Support from "./support";
import Navbar from "../../components/Navbar";
import { Link } from "react-router-dom";
const Dashboard: React.FC = () => {
return (
<div>
<h1 className="text-3xl font-bold mb-4">Личный кабинет</h1>
<div className="flex space-x-4 mb-4">
<Link className="underline" to="servers">Сервера</Link>
<Link className="underline" to="billing">Биллинг</Link>
<Link className="underline" to="support">Поддержка</Link>
</div>
<Routes>
<Route path="servers" element={<Servers />} />
<Route path="billing" element={<Billing />} />
<Route path="support" element={<Support />} />
<Route path="/" element={<div>Выберите раздел</div>} />
</Routes>
<div className="text-gray-800 min-h-screen bg-gray-50">
<Navbar />
<section className="pt-32 px-4 container mx-auto">
<h1 className="text-4xl font-bold mb-6">Личный кабинет</h1>
<div className="grid md:grid-cols-3 gap-6">
<Link
to="/dashboard/servers"
className="bg-white rounded-3xl p-6 shadow hover:shadow-2xl transition-all text-center"
>
Мои серверы
</Link>
<Link
to="/dashboard/billing"
className="bg-white rounded-3xl p-6 shadow hover:shadow-2xl transition-all text-center"
>
Баланс и платежи
</Link>
<Link
to="/dashboard/support"
className="bg-white rounded-3xl p-6 shadow hover:shadow-2xl transition-all text-center"
>
Поддержка
</Link>
</div>
</section>
</div>
);
};

View File

@@ -1,5 +1,17 @@
import Navbar from "../../components/Navbar";
const Servers: React.FC = () => {
return <div>Здесь будут ваши VPS</div>;
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<section className="pt-32 px-4 container mx-auto">
<h1 className="text-3xl font-bold mb-6">Мои VPS</h1>
<div className="bg-white rounded-3xl shadow p-6">
<p>Список серверов пока пуст...</p>
</div>
</section>
</div>
);
};
export default Servers;

View File

@@ -1,5 +1,17 @@
import React from 'react';
import Navbar from "../../components/Navbar";
export default function Support() {
return <div>Support tickets</div>;
}
const Support: React.FC = () => {
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<section className="pt-32 px-4 container mx-auto">
<h1 className="text-3xl font-bold mb-6">Поддержка</h1>
<div className="bg-white rounded-3xl shadow p-6">
<p>Здесь будет форма обращения в поддержку.</p>
</div>
</section>
</div>
);
};
export default Support;

View File

@@ -1,16 +1,110 @@
import React from 'react';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
import Navbar from "../components/Navbar";
import { Link } from "react-router-dom";
import { motion } from "framer-motion"; // анимации
export default function Index() {
const Home: React.FC = () => {
return (
<div className="min-h-screen flex flex-col bg-white">
<Navbar user={null} logout={() => {}} />
<main className="flex-grow p-10">
<h1 className="text-4xl font-bold">Добро пожаловать в ospab.host</h1>
<p className="mt-4 text-lg">Ваша панель управления VPS-хостингом</p>
</main>
<Footer />
<div className="text-gray-800">
<Navbar />
{/* Hero section */}
<section className="bg-gradient-to-r from-blue-600 to-indigo-700 text-white py-36">
<div className="container mx-auto text-center px-4">
<motion.h1
className="text-5xl font-bold mb-4 rounded-lg"
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1 }}
>
ospab.host Надёжный VPS и хостинг
</motion.h1>
<motion.p
className="text-xl mb-8"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 0.5 }}
>
Создавайте серверы, управляйте VPS и следите за метриками прямо из панели.
</motion.p>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5, delay: 1 }}
>
<Link
to="/login"
className="bg-white text-blue-700 font-bold py-3 px-6 rounded-full shadow-lg hover:bg-pink-500 hover:text-white transition-all"
>
Начать бесплатно
</Link>
</motion.div>
</div>
</section>
{/* Features section */}
<section className="py-20 bg-gray-100">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-12">Почему выбирают ospab.host</h2>
<div className="grid md:grid-cols-3 gap-8 text-center">
{[
{
title: "VPS на Proxmox",
desc: "Мгновенное создание и управление VPS, автоматические snapshot'ы и бэкапы.",
},
{
title: "Мониторинг и графики",
desc: "Полная информация о нагрузке CPU, RAM и дисков в реальном времени через ЛК.",
},
{
title: "Безопасность",
desc: "SSL, защита API, role-based access и безопасное хранение данных.",
},
].map((feature, idx) => (
<motion.div
key={idx}
className="bg-white rounded-3xl shadow p-6 hover:shadow-2xl transition-all"
whileHover={{ scale: 1.05 }}
>
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
<p>{feature.desc}</p>
</motion.div>
))}
</div>
</div>
</section>
{/* Pricing teaser */}
<section className="py-16">
<div className="container mx-auto px-4 text-center">
<h2 className="text-3xl font-bold mb-6">Прозрачные тарифы</h2>
<p className="mb-8">Выбирайте тариф под свои задачи, платите только за то, что используете.</p>
<Link
to="/pricing"
className="bg-blue-600 text-white font-bold py-3 px-6 rounded-full shadow hover:bg-pink-500 transition-all"
>
Посмотреть тарифы
</Link>
</div>
</section>
{/* Call-to-action */}
<section className="py-8 bg-indigo-700 text-white text-center rounded-t-3xl">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold mb-4">Готовы начать?</h2>
<p className="mb-6">
Создайте аккаунт и получите доступ к панели управления VPS уже сегодня.
</p>
<Link
to="/login"
className="bg-white text-indigo-700 font-bold py-3 px-6 rounded-full shadow hover:bg-pink-500 hover:text-white transition-all"
>
Зарегистрироваться
</Link>
</div>
</section>
</div>
);
}
};
export default Home;

View File

@@ -1,45 +1,53 @@
import Navbar from "../components/Navbar";
import { useState } from "react";
import { useAuth } from "../context/AuthContext";
import { useNavigate } from "react-router-dom";
interface LoginProps {
login: (email: string) => void;
}
const Login: React.FC<LoginProps> = ({ login }) => {
const Login: React.FC = () => {
const { login } = useAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
login(email);
navigate("/dashboard");
if (login(email, password)) {
navigate("/dashboard");
} else {
alert("Неверный логин или пароль");
}
};
return (
<div className="max-w-md mx-auto mt-20 p-6 bg-gray-800 text-white rounded">
<h1 className="text-2xl font-bold mb-4">Вход</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 rounded text-black"
required
/>
<input
type="password"
placeholder="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 rounded text-black"
required
/>
<button className="w-full bg-blue-600 py-2 rounded font-bold hover:bg-blue-700">
Войти
</button>
</form>
<div className="min-h-screen bg-gray-50">
<Navbar />
<section className="flex justify-center items-center h-screen">
<div className="bg-white rounded-3xl shadow-lg p-10 w-full max-w-md">
<h2 className="text-3xl font-bold mb-6 text-center">Вход в ЛК</h2>
<form className="space-y-4" onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-pink-500 outline-none"
/>
<input
type="password"
placeholder="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-pink-500 outline-none"
/>
<button
type="submit"
className="w-full bg-indigo-700 text-white py-2 rounded-full hover:bg-pink-500 transition-all"
>
Войти
</button>
</form>
</div>
</section>
</div>
);
};

View File

@@ -1,36 +1,44 @@
import React from 'react';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
import Button from '../components/Button';
import Navbar from "../components/Navbar";
import { motion } from "framer-motion";
export default function Pricing() {
const Pricing: React.FC = () => {
const plans = [
{ name: "Мини", price: "200 р/мес", features: ["1 vCPU", "1GB RAM", "25GB SSD"] },
{ name: "Стандарт", price: "500 р/мес", features: ["2 vCPU", "2GB RAM", "50GB SSD"] },
{ name: "Профессионал", price: "700 р/мес", features: ["4 vCPU", "8GB RAM", "100GB SSD"] },
{ title: "Basic VPS", price: "$5/мес", features: ["1 CPU", "1 GB RAM", "20 GB SSD"] },
{ title: "Pro VPS", price: "$15/мес", features: ["2 CPU", "4 GB RAM", "50 GB SSD"] },
{ title: "Enterprise", price: "$30/мес", features: ["4 CPU", "8 GB RAM", "100 GB SSD"] },
];
return (
<div className="min-h-screen flex flex-col bg-gray-50">
<Navbar user={null} logout={() => {}} />
<div className="text-gray-800">
<Navbar />
<main className="flex-grow px-6 py-12 text-center">
<h1 className="text-4xl font-bold mb-6">Выбери свой тариф</h1>
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
{plans.map(plan => (
<div key={plan.name} className="bg-white shadow-md rounded-xl p-6 hover:shadow-xl transition">
<h2 className="text-2xl font-bold mb-4">{plan.name}</h2>
<p className="text-3xl font-extrabold text-blue-600 mb-4">{plan.price}</p>
<ul className="text-gray-600 mb-6 space-y-2">
{plan.features.map(f => <li key={f}> {f}</li>)}
</ul>
<Button>Заказать</Button>
</div>
))}
<section className="py-24 bg-gray-100">
<div className="container mx-auto text-center px-4">
<h1 className="text-4xl font-bold mb-12">Наши тарифы</h1>
<div className="grid md:grid-cols-3 gap-8">
{plans.map((plan, idx) => (
<motion.div
key={idx}
className="bg-white rounded-3xl shadow p-6 hover:shadow-2xl transition-all"
whileHover={{ scale: 1.05 }}
>
<h2 className="text-2xl font-bold mb-4">{plan.title}</h2>
<p className="text-xl mb-4">{plan.price}</p>
<ul className="mb-6">
{plan.features.map((f, i) => (
<li key={i} className="mb-1">{f}</li>
))}
</ul>
<button className="bg-indigo-700 text-white py-2 px-6 rounded-full hover:bg-pink-500 transition-all">
Выбрать
</button>
</motion.div>
))}
</div>
</div>
</main>
<Footer />
</section>
</div>
);
}
};
export default Pricing;

View File

@@ -0,0 +1,77 @@
import { useState } from "react";
import { useAuth } from "../context/AuthContext";
import { useNavigate } from "react-router-dom";
import Navbar from "../components/Navbar";
export default function Register() {
const { register } = useAuth();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [password2, setPassword2] = useState("");
const navigate = useNavigate();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name || !email || !password || !password2) {
alert("Заполните все поля");
return;
}
if (password !== password2) {
alert("Пароли не совпадают");
return;
}
if (register(name, email, password, password2)) {
navigate("/dashboard");
} else {
alert("Ошибка регистрации");
}
};
return (
<div className="min-h-screen bg-gray-50">
<Navbar />
<section className="flex justify-center items-center h-screen">
<div className="bg-white rounded-3xl shadow-lg p-10 w-full max-w-md">
<h2 className="text-3xl font-bold mb-6 text-center">Регистрация</h2>
<form className="space-y-4" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Имя"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-pink-500 outline-none"
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-pink-500 outline-none"
/>
<input
type="password"
placeholder="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-pink-500 outline-none"
/>
<input
type="password"
placeholder="Повторите пароль"
value={password2}
onChange={(e) => setPassword2(e.target.value)}
className="w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-pink-500 outline-none"
/>
<button
type="submit"
className="w-full bg-indigo-700 text-white py-2 rounded-full hover:bg-pink-500 transition-all"
>
Зарегистрироваться
</button>
</form>
</div>
</section>
</div>
);
}