From 4f56f4bbc5bdbaaf0719237e7816029200f02475 Mon Sep 17 00:00:00 2001 From: Vontor Bruno Date: Mon, 5 Jan 2026 11:53:44 +0100 Subject: [PATCH] Implement user authentication and account pages Adds AuthContext with Orval API integration, user login/logout/register flows, and account settings page. Refactors navigation and layouts to use authentication context. Introduces new account-related pages (Login, Register, Logout, AccountSettings), updates PrivateRoute to use AuthContext, and improves error handling in Downloader. Removes obsolete context example and adds comprehensive usage documentation for AuthContext. --- frontend/src/App.tsx | 43 ++- frontend/src/components/navbar/SiteNav.tsx | 30 +- frontend/src/context/AuthContext.tsx | 119 +++--- frontend/src/context/Context.md | 30 -- frontend/src/context/EXAMPLES.md | 344 ++++++++++++++++++ frontend/src/layouts/AuthLayout.tsx | 15 + frontend/src/layouts/ChatLayout.tsx | 11 +- frontend/src/layouts/HomeLayout.tsx | 11 +- frontend/src/main.tsx | 5 +- .../src/pages/account/AccountSettings.tsx | 224 ++++++++++++ frontend/src/pages/account/Login.tsx | 108 ++++++ frontend/src/pages/account/Logout.tsx | 26 ++ frontend/src/pages/account/Register.tsx | 186 ++++++++++ frontend/src/pages/downloader/Downloader.tsx | 9 +- frontend/src/routes/PrivateRoute.tsx | 32 +- 15 files changed, 1030 insertions(+), 163 deletions(-) delete mode 100644 frontend/src/context/Context.md create mode 100644 frontend/src/context/EXAMPLES.md create mode 100644 frontend/src/pages/account/AccountSettings.tsx create mode 100644 frontend/src/pages/account/Login.tsx create mode 100644 frontend/src/pages/account/Logout.tsx create mode 100644 frontend/src/pages/account/Register.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7673480..360e406 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,33 +9,42 @@ import DroneServisSection from "./pages/home/components/Services/droneServis"; import PrivateRoute from "./routes/PrivateRoute"; -//import { UserContextProvider } from "./context/UserContext"; - // Pages import PortfolioPage from "./pages/portfolio/PortfolioPage"; import ContactPage from "./pages/contact/ContactPage"; import ScrollToTop from "./components/common/ScrollToTop"; + +import AuthLayout from "./layouts/AuthLayout"; +import LogoutPage from "./pages/account/Logout"; +import LoginPage from "./pages/account/Login"; +import RegisterPage from "./pages/account/Register"; + + export default function App() { return ( - {/* */} - - - {/* Public routes */} - }> - } /> - } /> - } /> + + + {/* Public routes */} + }> + } /> + } /> + } /> - {/* APPS */} - } /> + {/* APPS */} + } /> - {/* SERVICES */} - } /> - } /> + {/* SERVICES */} + } /> + } /> - + + + }> + } /> + } /> + } /> {/* Example protected route group (kept for future use) */} @@ -43,10 +52,8 @@ export default function App() { }> - - {/* */} ); } \ No newline at end of file diff --git a/frontend/src/components/navbar/SiteNav.tsx b/frontend/src/components/navbar/SiteNav.tsx index 7f3f063..542f0ff 100644 --- a/frontend/src/components/navbar/SiteNav.tsx +++ b/frontend/src/components/navbar/SiteNav.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { FaUserCircle, FaSignOutAlt, @@ -14,21 +15,18 @@ import { FaHandsHelping, } from "react-icons/fa"; import {FaClapperboard, FaCubes} from "react-icons/fa6"; +import { useAuth } from "@/context/AuthContext"; import styles from "./navbar.module.css"; -export interface User { - username: string; - email?: string; - avatarUrl?: string; -} - -interface NavbarProps { - user: User | null; - onLogin: () => void; - onLogout: () => void; -} - -export default function Navbar({ user, onLogin, onLogout }: NavbarProps) { +export default function Navbar() { + const { user, isAuthenticated, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogin = () => navigate("/login"); + const handleLogout = async () => { + await logout(); + navigate("/"); + }; const [mobileMenu, setMobileMenu] = useState(false); const navRef = useRef(null); @@ -114,8 +112,8 @@ export default function Navbar({ user, onLogin, onLogout }: NavbarProps) { Kontakt {/* right: user area */} - {!user ? ( - + {!isAuthenticated ? ( + ) : ( @@ -138,7 +136,7 @@ export default function Navbar({ user, onLogin, onLogout }: NavbarProps) { Nastavení Platby - diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 4bb0f0d..b65e8c4 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -1,66 +1,80 @@ -// import type { ReactNode } from "react"; -// import { createContext, useContext, useState, useEffect } from "react"; +import type { ReactNode } from "react"; +import { createContext, useContext, useState, useEffect } from "react"; -//TODO: připraveno pro použití jenom linknout funkce z vygenerovaného api klientan a logout() a currentUser() +// Import z Orval generovaného API +import { + apiAccountLoginCreate, + apiAccountLogoutCreate +} from "@/api/generated/public/account"; +import { apiAccountUserMeRetrieve } from "@/api/generated/private/account/account"; -//import { authLogin, authMe } from "../api/generated"; // your orval client -//import { LoginSchema } from "../api/generated/types"; -/* -export interface User { - id: number; - username: string; - email: string; - avatar?: string; -} +// Import typů z Orval +import type { CustomTokenObtainPair } from "@/api/generated/public/models/customTokenObtainPair"; +import type { CustomUser } from "@/api/generated/private/models/customUser"; interface AuthContextType { - user: User | null; + user: CustomUser | null; isAuthenticated: boolean; - login: (payload: LoginSchema) => Promise; - logout: () => void; + isLoading: boolean; + login: (payload: CustomTokenObtainPair) => Promise; + logout: () => Promise; refreshUser: () => Promise; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(null); + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); const isAuthenticated = !!user; - // load user when app mounts + // Načíst uživatele při načtení aplikace (pokud má cookies) useEffect(() => { - const token = localStorage.getItem("access"); - if (token) refreshUser(); + refreshUser(); }, []); async function refreshUser() { try { - const { data } = await authMe(); // ORVAL HANDLES TYPING - setUser(data); - } catch { + const userData = await apiAccountUserMeRetrieve(); + setUser(userData); + } catch (err: any) { + const errorMessage = err.response?.data?.error || err.message; + console.error("Failed to refresh user:", errorMessage); + setUser(null); + } finally { + setIsLoading(false); + } + } + + async function login(payload: CustomTokenObtainPair) { + setIsLoading(true); + try { + // Login endpoint automaticky nastaví HttpOnly cookies + await apiAccountLoginCreate(payload); + + // Po úspěšném přihlášení načti informace o uživateli + await refreshUser(); + } catch (err: any) { + setIsLoading(false); + const errorMessage = err.response?.data?.error || err.message; + throw new Error(errorMessage); + } + } + + async function logout() { + try { + // Zavolej logout endpoint (smaže cookies na backendu) + await apiAccountLogoutCreate(); + } catch (err: any) { + console.error("Logout error:", err); + } finally { setUser(null); } } - async function login(payload: LoginSchema) { - const { data } = await authLogin(payload); - - // example response: { access: "...", refresh: "..." } - localStorage.setItem("access", data.access); - localStorage.setItem("refresh", data.refresh); - - await refreshUser(); - } - - function logout() { - localStorage.removeItem("access"); - localStorage.removeItem("refresh"); - setUser(null); - } - return ( - + {children} ); @@ -70,29 +84,4 @@ export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); return ctx; -} -*/ - - - -/* -EXAMPLES OF USE: - -Login -const { data } = await authLogin(payload); // získám tokeny -localStorage.setItem("access", data.access); -localStorage.setItem("refresh", data.refresh); - -await authMe(); // poté zjistím, kdo to je - -Refresh -const { data } = await authRefresh({ refresh }); -localStorage.setItem("access", data.access); -await authMe(); - -Me (load user) -const { data: user } = await authMe(); -setUser(user); - - -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/context/Context.md b/frontend/src/context/Context.md deleted file mode 100644 index e35535f..0000000 --- a/frontend/src/context/Context.md +++ /dev/null @@ -1,30 +0,0 @@ -/* -# EXAMPLE USAGE OF CONTEXT IN A COMPONENT: - -## Wrap your app tree with the provider (e.g., in App.tsx) - -```tsx -import { UserContextProvider } from "../context/UserContext"; -function App() { - return ( - - - - ); -} -``` - - - -## Consume in any child component -```tsx -import React, { useContext } from "react" -import { UserContext } from '../context/UserContext'; - -export default function ExampleComponent() { - const { user, setUser } = useContext(UserContext); - - - return ...; -} -``` \ No newline at end of file diff --git a/frontend/src/context/EXAMPLES.md b/frontend/src/context/EXAMPLES.md new file mode 100644 index 0000000..ee0825d --- /dev/null +++ b/frontend/src/context/EXAMPLES.md @@ -0,0 +1,344 @@ +# AuthContext - Příklady použití + +## 📦 Setup v aplikaci + +Nejdříve obal celou aplikaci v `AuthProvider`: + +```tsx +// main.tsx nebo App.tsx +import { AuthProvider } from "@/context/AuthContext"; +import { BrowserRouter } from "react-router-dom"; + +function App() { + return ( + + + {/* Tady tvoje routes a komponenty */} + + + ); +} +``` + +--- + +## 🔐 Login stránka + +```tsx +import { useState } from "react"; +import { useAuth } from "@/context/AuthContext"; +import { useNavigate } from "react-router-dom"; + +export default function LoginPage() { + const { login, isLoading } = useAuth(); + const navigate = useNavigate(); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + + try { + // Login automaticky nastaví cookies a načte user data + await login({ username, password }); + navigate("/dashboard"); // Přesměruj po úspěšném přihlášení + } catch (err: any) { + setError(err.message); + } + } + + return ( +
+

Přihlášení

+ + {error && ( +
+ {error} +
+ )} + +
+ + setUsername(e.target.value)} + placeholder="username nebo email@example.com" + className="w-full p-2 border rounded" + required + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full p-2 border rounded" + required + /> +
+ + +
+ ); +} +``` + +--- + +## 👤 Profilová stránka + +```tsx +import { useAuth } from "@/context/AuthContext"; +import { Navigate } from "react-router-dom"; + +export default function ProfilePage() { + const { user, isAuthenticated, isLoading, logout } = useAuth(); + + // Redirect pokud není přihlášen + if (!isLoading && !isAuthenticated) { + return ; + } + + // Loading state + if (isLoading) { + return
Načítání...
; + } + + return ( +
+

Můj profil

+ +
+
+ Username: {user?.username} +
+
+ Email: {user?.email} +
+
+ Jméno: {user?.first_name} {user?.last_name} +
+
+ Role: {user?.role || "Uživatel"} +
+
+ Email ověřen: {user?.email_verified ? "✅ Ano" : "❌ Ne"} +
+
+ Účet vytvořen:{" "} + {user?.create_time ? new Date(user.create_time).toLocaleDateString("cs-CZ") : "N/A"} +
+ + +
+
+ ); +} +``` + +--- + +## 🛡️ Protected Route (ochrana stránek) + +```tsx +import { Navigate } from "react-router-dom"; +import { useAuth } from "@/context/AuthContext"; + +export function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return
Načítání...
; + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} + +// Použití v routes: +// } /> +``` + +--- + +## 🧭 Navigace s uživatelem + +```tsx +import { useAuth } from "@/context/AuthContext"; +import { Link } from "react-router-dom"; + +export function Navbar() { + const { user, isAuthenticated, logout } = useAuth(); + + return ( + + ); +} +``` + +--- + +## 🔄 Manuální refresh uživatele + +```tsx +import { useAuth } from "@/context/AuthContext"; +import { useEffect } from "react"; + +export function SettingsPage() { + const { user, refreshUser } = useAuth(); + + // Po úpravě profilu obnov data + async function handleUpdateProfile() { + // ... ulož změny přes API + + // Pak obnov context + await refreshUser(); + } + + return ( +
+

Nastavení profilu

+ {/* formulář pro úpravu */} + +
+ ); +} +``` + +--- + +## 📊 Podmíněné zobrazení podle role + +```tsx +import { useAuth } from "@/context/AuthContext"; + +export function AdminPanel() { + const { user } = useAuth(); + + // Zobraz jen pro adminy + if (user?.role !== "admin") { + return
Nemáte oprávnění k přístupu.
; + } + + return ( +
+

Admin Panel

+ {/* Admin obsah */} +
+ ); +} +``` + +--- + +## 🔑 Dostupné API typy + +### CustomTokenObtainPair (Login payload) +```typescript +{ + username: string; // nebo email + password: string; +} +``` + +### CustomUser (Response type) +```typescript +{ + id: number; + username: string; + email: string; + first_name?: string; + last_name?: string; + role?: "admin" | "user" | ...; + email_verified?: boolean; + phone_number?: string | null; + create_time: Date; + city?: string | null; + street?: string | null; + postal_code?: string | null; + gdpr: boolean; + is_active?: boolean; +} +``` + +--- + +## ⚙️ AuthContext API + +### `user: CustomUser | null` +Aktuálně přihlášený uživatel nebo `null` + +### `isAuthenticated: boolean` +`true` pokud je uživatel přihlášen + +### `isLoading: boolean` +`true` během načítání uživatele + +### `login(payload: CustomTokenObtainPair): Promise` +Přihlásit uživatele (nastaví cookies a načte data) + +### `logout(): Promise` +Odhlásit uživatele (smaže cookies) + +### `refreshUser(): Promise` +Znovu načti aktuální uživatelská data z API + +--- + +## 🎯 Best Practices + +1. **Vždy používej `isLoading`** pro zobrazení loading stavu +2. **Kontroluj `isAuthenticated`** před přístupem k chráněnému obsahu +3. **Zachyť chyby** při login (try/catch) +4. **Po úpravě profilu** zavolej `refreshUser()` +5. **Používej `ProtectedRoute`** pro ochranu celých stránek diff --git a/frontend/src/layouts/AuthLayout.tsx b/frontend/src/layouts/AuthLayout.tsx index e69de29..3f278cf 100644 --- a/frontend/src/layouts/AuthLayout.tsx +++ b/frontend/src/layouts/AuthLayout.tsx @@ -0,0 +1,15 @@ +import Footer from "../components/Footer/footer"; +import { Outlet } from "react-router"; +import SiteNav from "../components/navbar/SiteNav"; + +import styles from "./HomeLayout.module.css"; + +export default function AuthLayout(){ + return( +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/layouts/ChatLayout.tsx b/frontend/src/layouts/ChatLayout.tsx index 706bd8f..1b140a1 100644 --- a/frontend/src/layouts/ChatLayout.tsx +++ b/frontend/src/layouts/ChatLayout.tsx @@ -1,20 +1,13 @@ import Footer from "../components/Footer/footer"; import { Outlet } from "react-router"; -import SiteNav, { type User } from "../components/navbar/SiteNav"; - - -const userexists: User = { - username: "Bruno", - email: "", - avatarUrl: "", -}; +import SiteNav from "../components/navbar/SiteNav"; import styles from "./HomeLayout.module.css"; export default function ChatLayout(){ return(
- {}} onLogout={() => {}} /> +
) diff --git a/frontend/src/layouts/HomeLayout.tsx b/frontend/src/layouts/HomeLayout.tsx index a94a094..338a60b 100644 --- a/frontend/src/layouts/HomeLayout.tsx +++ b/frontend/src/layouts/HomeLayout.tsx @@ -1,20 +1,13 @@ import Footer from "../components/Footer/footer"; import { Outlet } from "react-router"; -import SiteNav, { type User } from "../components/navbar/SiteNav"; - - -const userexists: User = { - username: "Bruno", - email: "", - avatarUrl: "", -}; +import SiteNav from "../components/navbar/SiteNav"; import styles from "./HomeLayout.module.css"; export default function HomeLayout(){ return(
- {}} onLogout={() => {}} /> +
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2673af0..709ce60 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,11 +1,14 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { AuthProvider } from './context/AuthContext' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/frontend/src/pages/account/AccountSettings.tsx b/frontend/src/pages/account/AccountSettings.tsx new file mode 100644 index 0000000..d820f9b --- /dev/null +++ b/frontend/src/pages/account/AccountSettings.tsx @@ -0,0 +1,224 @@ +import { useState } from "react"; +import { useAuth } from "@/context/AuthContext"; +import { Navigate } from "react-router-dom"; +import { FaUser, FaEnvelope, FaPhone, FaMapMarkerAlt, FaCheckCircle, FaSpinner } from "react-icons/fa"; +import { apiAccountUsersPartialUpdate } from "@/api/generated/private/account/account"; + +export default function AccountSettings() { + const { user, isAuthenticated, isLoading: authLoading, refreshUser } = useAuth(); + + const [formData, setFormData] = useState({ + first_name: user?.first_name || "", + last_name: user?.last_name || "", + phone_number: user?.phone_number || "", + city: user?.city || "", + street: user?.street || "", + postal_code: user?.postal_code || "", + }); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + if (authLoading) { + return ( +
+ +
+ ); + } + + if (!isAuthenticated) { + return ; + } + + const handleChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); + }; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setSuccess(false); + setIsLoading(true); + + try { + if (!user?.id) throw new Error("User ID not found"); + + await apiAccountUsersPartialUpdate(user.id, formData); + await refreshUser(); + setSuccess(true); + setTimeout(() => setSuccess(false), 3000); + } catch (err: any) { + const errorMessage = err.response?.data?.error || err.message || "Nepodařilo se uložit změny"; + setError(errorMessage); + } finally { + setIsLoading(false); + } + } + + return ( +
+
+
+

Nastavení účtu

+

Upravte své osobní údaje

+ + {/* User info (read-only) */} +
+

Informace o účtu

+
+

Username: {user?.username}

+

Email: {user?.email}

+

Role: {user?.role || "user"}

+

Email ověřen: {user?.email_verified ? "✅ Ano" : "❌ Ne"}

+
+
+ + {success && ( +
+ + Změny byly úspěšně uloženy! +
+ )} + + {error && ( +
+ Chyba: {error} +
+ )} + +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +

Formát: +420123456789

+
+ +
+

+ + Adresa +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +

5 číslic bez mezery

+
+
+
+ +
+ +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/account/Login.tsx b/frontend/src/pages/account/Login.tsx new file mode 100644 index 0000000..a6adedb --- /dev/null +++ b/frontend/src/pages/account/Login.tsx @@ -0,0 +1,108 @@ +import { useState } from "react"; +import { useAuth } from "@/context/AuthContext"; +import { useNavigate, Link } from "react-router-dom"; +import { FaEnvelope, FaLock, FaSpinner } from "react-icons/fa"; + +export default function LoginPage() { + const { login, isLoading } = useAuth(); + const navigate = useNavigate(); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + + if (!username.trim() || !password.trim()) { + setError("Vyplňte prosím všechna pole"); + return; + } + + try { + await login({ username, password }); + navigate("/"); + } catch (err: any) { + const errorMessage = err.response?.data?.error || err.message; + setError(errorMessage); + } + } + + return ( +
+
+

Přihlášení

+

Vítejte zpět na vontor.cz

+ + {error && ( +
+ Chyba: {error} +
+ )} + +
+
+ + setUsername(e.target.value)} + placeholder="email@example.com nebo username" + className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition" + disabled={isLoading} + required + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition" + disabled={isLoading} + required + /> +
+ +
+ + Zapomenuté heslo? + +
+ + +
+ +
+ Ještě nemáte účet?{" "} + + Zaregistrujte se + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/account/Logout.tsx b/frontend/src/pages/account/Logout.tsx new file mode 100644 index 0000000..7f8fce5 --- /dev/null +++ b/frontend/src/pages/account/Logout.tsx @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import { useAuth } from "@/context/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { FaSpinner } from "react-icons/fa"; + +export default function LogoutPage() { + const { logout } = useAuth(); + const navigate = useNavigate(); + + useEffect(() => { + async function performLogout() { + await logout(); + navigate("/"); + } + performLogout(); + }, [logout, navigate]); + + return ( +
+
+ +

Odhlašování...

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/account/Register.tsx b/frontend/src/pages/account/Register.tsx new file mode 100644 index 0000000..8ccda22 --- /dev/null +++ b/frontend/src/pages/account/Register.tsx @@ -0,0 +1,186 @@ +import { useState } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { FaUser, FaEnvelope, FaLock, FaSpinner, FaCheckCircle } from "react-icons/fa"; +import { apiAccountRegisterCreate } from "@/api/generated/public/account"; + +export default function RegisterPage() { + const navigate = useNavigate(); + + const [formData, setFormData] = useState({ + username: "", + email: "", + password: "", + password2: "", + }); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); + }; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + + // Validation + if (!formData.username.trim() || !formData.email.trim() || !formData.password.trim()) { + setError("Vyplňte prosím všechna pole"); + return; + } + + if (formData.password !== formData.password2) { + setError("Hesla se neshodují"); + return; + } + + if (formData.password.length < 8) { + setError("Heslo musí mít alespoň 8 znaků"); + return; + } + + setIsLoading(true); + try { + await apiAccountRegisterCreate({ + username: formData.username, + email: formData.email, + password: formData.password, + password2: formData.password2, + }); + + setSuccess(true); + setTimeout(() => navigate("/login"), 2000); + } catch (err: any) { + const errorMessage = err.response?.data?.error || err.message || "Registrace se nezdařila"; + setError(errorMessage); + } finally { + setIsLoading(false); + } + } + + if (success) { + return ( +
+
+ +

Registrace úspěšná!

+

Váš účet byl vytvořen. Přesměrování na přihlášení...

+
+
+ ); + } + + return ( +
+
+

Registrace

+

Vytvořte si účet na vontor.cz

+ + {error && ( +
+ Chyba: {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +

Minimálně 8 znaků

+
+ +
+ + +
+ + +
+ +
+ Již máte účet?{" "} + + Přihlaste se + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/downloader/Downloader.tsx b/frontend/src/pages/downloader/Downloader.tsx index 11fb573..e05f5cc 100644 --- a/frontend/src/pages/downloader/Downloader.tsx +++ b/frontend/src/pages/downloader/Downloader.tsx @@ -95,7 +95,10 @@ export default function Downloader() { setSelectedVideos(info.videos.map((_, index) => index + 1)); } } catch (err: any) { - setError({ error: err.message || 'Failed to retrieve video info' }); + // Handle axios errors properly + const errorMessage = err.response?.data?.error || err.message + setError({ error: errorMessage }); + console.error('Retrieve video info error:', err); } finally { setIsLoading(false); } @@ -154,7 +157,9 @@ export default function Downloader() { window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (err: any) { - setError({ error: err.message || 'Failed to download video' }); + const errorMessage = err.response?.data?.error || err.message || 'Failed to download video'; + setError({ error: errorMessage }); + console.error('Download error:', err); } finally { setIsDownloading(false); } diff --git a/frontend/src/routes/PrivateRoute.tsx b/frontend/src/routes/PrivateRoute.tsx index 1b3a3c5..47558a8 100644 --- a/frontend/src/routes/PrivateRoute.tsx +++ b/frontend/src/routes/PrivateRoute.tsx @@ -1,22 +1,28 @@ import { Navigate, Outlet, useLocation } from "react-router-dom"; - -function getCookie(name: string): string | null { - const nameEQ = name + "="; - const ca = document.cookie.split(";").map((c) => c.trim()); - for (const c of ca) { - if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length)); - } - return null; -} - -const ACCESS_COOKIE = "access_token"; +import { useAuth } from "@/context/AuthContext"; +import { FaSpinner } from "react-icons/fa"; export default function PrivateRoute() { const location = useLocation(); - const isLoggedIn = !!getCookie(ACCESS_COOKIE); + const { isAuthenticated, isLoading } = useAuth(); - if (!isLoggedIn) { + // Zobraz loading během načítání uživatele + if (isLoading) { + return ( +
+
+ +

Načítání...

+
+
+ ); + } + + // Pokud není přihlášen, redirect na login (ulož původní cestu) + if (!isAuthenticated) { return ; } + + // Uživatel je přihlášen, renderuj chráněný obsah return ; }