From 99a164f08887306230185826eded944553a7bf1d Mon Sep 17 00:00:00 2001 From: Pc Date: Tue, 6 Jan 2026 16:04:47 +0100 Subject: [PATCH 1/2] feat: added random, word, custom generated links --- .../src/components/Generator.tsx | 435 ++++++++++++++++-- .../src/context/AuthProvider.tsx | 63 +-- 2 files changed, 409 insertions(+), 89 deletions(-) diff --git a/kittyurl-frontend/src/components/Generator.tsx b/kittyurl-frontend/src/components/Generator.tsx index 08c2bb4..e28f46e 100644 --- a/kittyurl-frontend/src/components/Generator.tsx +++ b/kittyurl-frontend/src/components/Generator.tsx @@ -1,60 +1,393 @@ -import React from 'react'; -import { PawPrint, Heart, Sparkles, Cat } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { + PawPrint, Heart, Sparkles, Cat, Hash, + Globe, BookOpen, Shield, Calendar, + Settings2, AlertCircle, X, Save, RefreshCw, Copy, Check, ExternalLink, User as UserIcon +} from 'lucide-react'; -interface GeneratorProps { - url: string; - setUrl: (val: string) => void; - onGenerate: () => void; +// Pobieramy adres API +const API_BASE = import.meta.env.VITE_API_TARGET; + +// Nazwa klucza w localStorage, gdzie trzymasz token +const TOKEN_KEY = 'jwt_token'; + +type CaseType = 'upper' | 'lower' | 'mixed'; + +interface GeneratorSettings { + length: number; + alphanum: boolean; + case: CaseType; + withSubdomain: boolean; } -export const Generator: React.FC = ({ url, setUrl, onGenerate }) => ( -
- {/* Header - Skalowanie tekstu i ikony */} -
-

- KittyURL -

-

- Shorten KKKKKK your links with a purr! -

-
+interface LinkFormData { + remoteUrl: string; + uri: string; + subdomain: string; + privacy: boolean; + expiryDate: string; +} - {/* Główna karta - mniejsze paddingi i zaokrąglenia na mobile */} -
-
- -
- setUrl(e.target.value)} - /> - +interface LinkPayload { + remoteUrl: string; + uri: string; + subdomain?: string; + privacy: boolean; + expiryDate: number; + userId?: string; +} + +interface User { + id: string; + username: string; + email?: string; +} + +export const Generator: React.FC = () => { + const [user, setUser] = useState(null); + + // Stan formularza głównego + const [formData, setFormData] = useState({ + remoteUrl: '', + uri: '', + subdomain: '', + privacy: true, + expiryDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + }); + + // Stan ustawień generatora (GET) + const [genSettings, setGenSettings] = useState({ + length: 6, + alphanum: true, + case: 'mixed', + withSubdomain: false + }); + + // Stany UI + const [loading, setLoading] = useState(false); + const [generatingUri, setGeneratingUri] = useState(false); + const [errorMsg, setErrorMsg] = useState(null); + const [result, setResult] = useState(null); + const [copied, setCopied] = useState(false); + + // Helper do pobierania nagłówków z tokenem + const getAuthHeaders = () => { + const token = localStorage.getItem(TOKEN_KEY); + return { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; + }; + + // 1. Sprawdzenie sesji użytkownika przy starcie (używając JWT) + // 1. Sprawdzenie sesji użytkownika przy starcie + useEffect(() => { + const checkUser = async () => { + const token = localStorage.getItem(TOKEN_KEY); + + // Jeśli brak tokena, przerywamy (tryb gościa) + if (!token) return; + + try { + const res = await fetch(`${API_BASE}/api/v1/user/account`, { + headers: getAuthHeaders() + }); + + if (res.ok) { + const data = await res.json(); + + // Naprawa "Logged in as undefined": + // API może zwracać 'name', 'username' lub tylko 'email' + setUser({ + id: data.id || data._id || data.userId, + username: data.username || data.name || data.email || "User", + email: data.email + }); + } else { + // Jeśli token jest nieważny (401), czyścimy go + console.log("Session expired. Logging out."); + localStorage.removeItem(TOKEN_KEY); + setUser(null); + } + } catch { + // POPRAWKA: Usunęliśmy '(err)', teraz jest samo 'catch' + // Dzięki temu linter nie krzyczy o nieużywaną zmienną + console.log("API unreachable"); + } + }; + checkUser(); + }, []); + + // 2. Generowanie URI (GET) - tutaj auth zazwyczaj nie jest wymagany, ale można dodać + const handleGenerateUri = async (type: 'random' | 'wordlist') => { + setGeneratingUri(true); + setErrorMsg(null); + try { + let endpoint = ''; + const params = new URLSearchParams(); + + params.append('withSubdomain', genSettings.withSubdomain.toString()); + + if (type === 'random') { + endpoint = '/api/v1/link/short'; + params.append('length', genSettings.length.toString()); + params.append('alphanum', genSettings.alphanum.toString()); + + if (genSettings.case !== 'mixed') { + params.append('case', genSettings.case); + } + } else { + endpoint = '/api/v1/link/fromWordlist'; + } + + // GET zazwyczaj jest publiczny, więc nie musimy dodawać Bearera, + // ale jeśli API tego wymaga, dodaj: headers: getAuthHeaders() + const response = await fetch(`${API_BASE}${endpoint}?${params.toString()}`); + const data = await response.json(); + + if (!response.ok) throw new Error(data.error || 'Generation failed'); + + const generatedUri = data.uri || data.shortUrl || data.link || ""; + setFormData(prev => ({ ...prev, uri: generatedUri })); + + } catch (err: unknown) { + if (err instanceof Error) setErrorMsg(err.message); + } finally { + setGeneratingUri(false); + } + }; + + // 3. Zapis do bazy (POST) - WYMAGA AUTH (JWT) + const handleSubmitToDb = async () => { + if (!formData.remoteUrl) { + setErrorMsg("Meow! I need a destination URL first! 🐾"); + return; + } + if (!formData.uri) { + setErrorMsg("Please generate or write a short URI code!"); + return; + } + + setLoading(true); + setErrorMsg(null); + setResult(null); + + try { + const payload: LinkPayload = { + remoteUrl: formData.remoteUrl, + uri: formData.uri, + subdomain: formData.subdomain || undefined, + privacy: formData.privacy, + expiryDate: new Date(formData.expiryDate).getTime() + }; + + if (user && user.id) { + payload.userId = user.id; + } + + const response = await fetch(`${API_BASE}/api/v1/link/new`, { + method: 'POST', + headers: getAuthHeaders(), // Tu wstrzykujemy JWT + body: JSON.stringify(payload) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || `Database error ${response.status}`); + } + + const finalLink = data.url || `${API_BASE.replace('api.', '')}/${formData.uri}`; + setResult(finalLink); + + } catch (err: unknown) { + if (err instanceof Error) setErrorMsg(err.message); + else setErrorMsg("Something went wrong saving to DB!"); + } finally { + setLoading(false); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ {/* Header */} +
+
+ + {user ? `Logged in as ${user.username}` : 'Guest Mode (Anonymous)'}
+ +

+ KittyURL +

+ +
+ + {/* Error Display */} + {errorMsg && ( +
+
+ + {errorMsg} + +
+
+ )} + + {/* Success Result Card */} + {result && ( +
+
+
+
+
+

Saved to Database!

+

{result}

+
+
+ + + + +
+
+
+
+ )} + + {/* Main Form */} +
+ + {/* 1. Destination URL */} +
+ +
+ setFormData({ ...formData, remoteUrl: e.target.value })} + /> + +
+
+ + {/* 2. Short Code Generation */} +
+ + +
+
+ + setGenSettings({ ...genSettings, length: +e.target.value })} /> +
+
+ + +
+ + +
+ +
+ setFormData({ ...formData, uri: e.target.value })} + /> +
/
+ {formData.uri && ( +
+ +
+ )} +
+
+ + {/* 3. Database Settings */} +
+
+ + setFormData({ ...formData, expiryDate: e.target.value })} /> +
+ + + + +
+ + {/* 4. Submit Button */} + +
- -
- - {/* Sekcja "No links yet" - Skalowanie paddingu i ikony */} -
-
- -

- No links generated yet. Feed me a URL! 🐾 +

+ +

+ KittyURL Generator v2.0

-
+
-
-); \ No newline at end of file + ); +}; \ No newline at end of file diff --git a/kittyurl-frontend/src/context/AuthProvider.tsx b/kittyurl-frontend/src/context/AuthProvider.tsx index 9a6fc17..1e3886b 100644 --- a/kittyurl-frontend/src/context/AuthProvider.tsx +++ b/kittyurl-frontend/src/context/AuthProvider.tsx @@ -1,40 +1,18 @@ import { useState, useCallback, type ReactNode } from 'react'; -import Cookies from 'js-cookie'; import { AuthContext } from './AuthContext'; import { sha512 } from '../utils/crypto'; -const TOKEN_KEY = 'ktty_shared_token'; +// Nazwa klucza w localStorage (musi być spójna z Generator.tsx) +const TOKEN_KEY = 'jwt_token'; -const getCookieConfig = () => { - const hostname = window.location.hostname; - - // Sprawdzamy, czy jesteśmy na localhost - const isLocal = hostname === 'localhost' || hostname === '127.0.0.1'; - - // Sprawdzamy, czy połączenie jest bezpieczne (HTTPS) - const isSecure = window.location.protocol === 'https:'; - - return { - // Na produkcji używamy domeny nadrzędnej z kropką, by działało na subdomenach - // Na localhost MUSI być undefined, inaczej przeglądarka odrzuci ciasteczko - domain: isLocal ? undefined : '.ktty.is', - - // Atrybut Secure wymaga HTTPS. Na localhost wyłączamy, na produkcji włączamy. - secure: isSecure, - - // 'Lax' jest bezpieczne i pozwala na współdzielenie w obrębie subdomen. - // Jeśli API jest na zupełnie innej domenie, rozważ 'None' (wymaga Secure: true). - sameSite: 'Lax' as const, - - path: '/', - expires: 7 - }; -}; +// Adres API +const API_BASE = import.meta.env.VITE_API_TARGET || 'https://ktty.is'; export function AuthProvider({ children }: { children: ReactNode }) { - const [token, setToken] = useState(() => Cookies.get(TOKEN_KEY) || null); + // Inicjalizacja stanu + const [token, setToken] = useState(() => localStorage.getItem(TOKEN_KEY)); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); // DODANE + const [error, setError] = useState(null); const authRequest = useCallback(async (endpoint: 'signIn' | 'signUp', name: string, pass: string) => { setLoading(true); @@ -42,24 +20,26 @@ export function AuthProvider({ children }: { children: ReactNode }) { try { const hashedPassword = await sha512(pass); - const response = await fetch(`/api/v1/user/${endpoint}`, { + + const response = await fetch(`${API_BASE}/api/v1/user/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - credentials: 'include', body: JSON.stringify({ name, password: hashedPassword }), }); const data = await response.json(); if (!response.ok) { - throw new Error(data?.message || 'Błąd autoryzacji'); + throw new Error(data?.message || data?.error || 'Błąd autoryzacji'); } if (data?.token) { - Cookies.set(TOKEN_KEY, data.token, getCookieConfig()); + localStorage.setItem(TOKEN_KEY, data.token); setToken(data.token); } + return data; + } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Wystąpił błąd'; setError(msg); @@ -69,11 +49,18 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, []); + // ZMODYFIKOWANA FUNKCJA LOGOUT const logout = useCallback(() => { - const config = getCookieConfig(); - // When removing, you must match the domain and path used when setting - Cookies.remove(TOKEN_KEY, { domain: config.domain, path: config.path }); + // 1. Usuwamy token z pamięci + localStorage.removeItem(TOKEN_KEY); setToken(null); + + // 2. Wymuszamy odświeżenie aplikacji + // To jest "Hard Refresh", który czyści cały stan Reacta + window.location.reload(); + + // Opcjonalnie: Jeśli wolisz tylko przekierowanie na główną bez pełnego przeładowania (szybciej, ale zostawia stan w pamięci): + // window.location.href = '/'; }, []); return ( @@ -81,9 +68,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { isAuthenticated: !!token, token, loading, - error, + error, signIn: (n, p) => authRequest('signIn', n, p), - signUp: (n, p) => authRequest('signUp', n, p), + signUp: (n, p) => authRequest('signUp', n, p), logout }}> {children} -- 2.39.5 From 2dbed7cc7e547c48226471fbc68b97b17421ed12 Mon Sep 17 00:00:00 2001 From: Pc Date: Tue, 6 Jan 2026 16:40:33 +0100 Subject: [PATCH 2/2] fix: typo fix --- kittyurl-frontend/src/components/Generator.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/kittyurl-frontend/src/components/Generator.tsx b/kittyurl-frontend/src/components/Generator.tsx index e28f46e..ad4f235 100644 --- a/kittyurl-frontend/src/components/Generator.tsx +++ b/kittyurl-frontend/src/components/Generator.tsx @@ -226,6 +226,7 @@ export const Generator: React.FC = () => {

KittyURL

+

Shorten your links with a purr!

-- 2.39.5