diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index 09ff02c..f560aa9 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -117,8 +117,28 @@ input:invalid { box-shadow: none; } -input[type="password"]:focus::placeholder { +/* Standard placeholder */ +input:focus::placeholder { color: transparent; + opacity: 0; /* optional, for smoother fade in some browsers */ +} + +/* WebKit (Safari, Chrome, iOS) */ +input:focus::-webkit-input-placeholder { + color: transparent; + opacity: 0; +} + +/* Firefox */ +input:focus::-moz-placeholder { + color: transparent; + opacity: 0; +} + +/* Microsoft Edge / IE */ +input:focus:-ms-input-placeholder { + color: transparent; + opacity: 0; } /* Remove Safari input shadow on mobile */ @@ -213,6 +233,7 @@ Custom #lyric-search-input { margin-right: 1.5%; + padding-bottom: 1rem; } #lyrics-info { @@ -226,9 +247,17 @@ Custom } .lyrics-card { - border: 1px solid grey; - border-radius: 5px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.05); + padding: 1.5rem; + transition: background 0.3s; +} + +.lyrics-content { line-height: 2.0; + font-family: 'Inter', sans-serif; + font-size: 1rem; + white-space: pre-wrap; } .lyrics-card-dark { @@ -258,6 +287,17 @@ Custom overscroll-behavior: contain; } +.p-autocomplete-input { + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: 1px solid #ccc; + transition: border 0.2s; +} +.p-autocomplete-input:focus { + border-color: #4f46e5; + outline: none; +} + .d-dark > * { background-color: rgba(35, 35, 35, 0.9); color: #ffffff; @@ -266,12 +306,6 @@ Custom background-color: rgba(255, 255, 255, 0.9); } -.active-breadcrumb { - font-weight: bold; - text-decoration: underline; - text-underline-offset: 2px; /* makes it more visible */ -} - /* Toastify customizations */ diff --git a/src/assets/styles/player.css b/src/assets/styles/player.css index 469d26d..72ae265 100644 --- a/src/assets/styles/player.css +++ b/src/assets/styles/player.css @@ -116,7 +116,8 @@ body { } .station-tabs { - padding-top: 4%; + padding-top: 6%; + padding-bottom: 4%; } .music-player__album { diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx index 6228178..046d850 100644 --- a/src/components/AppLayout.jsx +++ b/src/components/AppLayout.jsx @@ -32,7 +32,7 @@ export default function Root({ child }) { color="danger"> Work in progress... bugs are to be expected. */} - {child == "LoginPage" && ()} + {child == "LoginPage" && ()} {child == "LyricSearch" && ()} {child == "Player" && ()} {child == "Memes" && } diff --git a/src/components/Login.jsx b/src/components/Login.jsx index 198934e..66fce5f 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { toast } from "react-toastify"; import { API_URL } from "@/config"; @@ -7,6 +7,14 @@ export default function LoginPage() { const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); + const passwordRef = useRef(); + + useEffect(() => { + if (passwordRef.current && password === "") { + passwordRef.current.value = ""; + } + }, []); + async function handleSubmit(e) { e.preventDefault(); setLoading(true); @@ -20,6 +28,7 @@ export default function LoginPage() { setLoading(false); return toast.error("Password is required"); } + const formData = new URLSearchParams(); formData.append("username", username); formData.append("password", password); @@ -30,10 +39,8 @@ export default function LoginPage() { const resp = await fetch(`${API_URL}/auth/login`, { method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - credentials: "include", // Important for cookies + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + credentials: "include", body: formData.toString(), }); @@ -44,21 +51,15 @@ export default function LoginPage() { } if (!resp.ok) { - if (resp.json().detail) { - toast.error(`Login failed: ${resp.json().detail}`); - } - else { - toast.error("Login failed"); - } + const data = await resp.json().catch(() => ({})); + toast.error(data.detail ? `Login failed: ${data.detail}` : "Login failed"); setLoading(false); return; } const data = await resp.json(); - if (data.access_token) { toast.success("Login successful!"); - // Redirect window.location.href = "/TRip"; // TODO: fix, hardcoded } else { toast.error("Login failed: no access token received"); @@ -72,21 +73,16 @@ export default function LoginPage() { } return ( -
+

Logo Authentication Required

-
-
- + + {/* Username */} +
setUsername(e.target.value)} required - className="appearance-none block w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-[#121212] dark:text-white" - placeholder="Your username" disabled={loading} + className="peer block w-full px-4 pt-5 pb-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-transparent text-gray-900 dark:text-white placeholder-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" /> +
-
- + {/* Password */} +
setPassword(e.target.value)} required - className="appearance-none block w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-[#121212] dark:text-white" - placeholder="••••••••" disabled={loading} + className="peer block w-full px-4 pt-5 pb-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-transparent text-gray-900 dark:text-white placeholder-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" /> +
- {track.audioQuality} + {quality} {track.version && ( ({track.version}) )} @@ -636,7 +648,6 @@ export default function MediaRequestForm() { ); })} -
@@ -368,12 +403,19 @@ export default function RequestManagement() {

)} + {selectedRequest.quality && ( +

Quality:{" "} + {selectedRequest.quality}

+ )}
) : (

Loading...

- )} - + ) + } + -
+
); } diff --git a/src/hooks/requireAuthHook.js b/src/hooks/requireAuthHook.js new file mode 100644 index 0000000..443bec8 --- /dev/null +++ b/src/hooks/requireAuthHook.js @@ -0,0 +1,58 @@ +export const requireAuthHook = async () => { + const token = Astro.cookies.get("access_token")?.value; + let user = null; + + try { + if (!token) throw Error("No access token"); + + // Step 1: verify current access token + user = verifyToken(token); + + if (!user) throw Error("Invalid access token"); + + console.log("Verified!", user); + + } catch (err) { + console.log("Access token check failed:", err.message); + + // Step 2: attempt refresh if refresh_token exists + const refreshToken = Astro.cookies.get("refresh_token")?.value; + if (refreshToken) { + try { + const newTokens = await refreshAccessToken(refreshToken); + if (newTokens?.accessToken) { + // store new access token + Astro.cookies.set("access_token", newTokens.accessToken, { + path: "/", + httpOnly: true, + sameSite: "lax", + secure: true, + }); + + // Optionally replace refresh_token too + if (newTokens.refreshToken) { + Astro.cookies.set("refresh_token", newTokens.refreshToken, { + path: "/", + httpOnly: true, + sameSite: "lax", + secure: true, + }); + } + + // re-verify user with new token + user = verifyToken(newTokens.accessToken); + + if (user) { + console.log("Refreshed + verified!", user); + return; // ✅ authenticated now + } + } + } catch (refreshErr) { + console.error("Refresh failed:", refreshErr.message); + } + } + + // Step 3: if still no user, redirect + return Astro.redirect("/login"); + } +} \ No newline at end of file diff --git a/src/layouts/Nav.astro b/src/layouts/Nav.astro index 53bcac2..45e3b6c 100644 --- a/src/layouts/Nav.astro +++ b/src/layouts/Nav.astro @@ -40,12 +40,15 @@ const currentPath = Astro.url.pathname; const isExternal = item.href?.startsWith("http"); const isAuthedPath = item.auth ?? false; - const normalize = (url) => url?.replace(/\/+$/, '') || '/'; - const normalizedCurrent = normalize(currentPath).replace(/\/$/, ""); // remove trailing slash - const normalizedHref = normalize(item.href).replace(/\/$/, ""); + const normalize = (url) => (url || '/').replace(/\/+$/, '') || '/'; + const normalizedCurrent = normalize(currentPath); + const normalizedHref = normalize(item.href); const isActive = !isExternal && ( - normalizedCurrent === normalizedHref || - normalizedCurrent.startsWith(normalizedHref + "/")); + normalizedHref === '/' + ? normalizedCurrent === '/' // Home only matches exact / + : normalizedCurrent === normalizedHref || normalizedCurrent.startsWith(normalizedHref + '/') + ); + const nextItem = navItems[index + 1]; const shouldShowThinBar = nextItem //&& !nextItem.blockSeparator; diff --git a/src/pages/TRip/index.astro b/src/pages/TRip/index.astro index ead22e6..56cb474 100644 --- a/src/pages/TRip/index.astro +++ b/src/pages/TRip/index.astro @@ -3,7 +3,9 @@ import MediaRequestForm from "@/components/TRip/MediaRequestForm" import Base from "@/layouts/Base.astro"; import Root from "@/components/AppLayout.jsx"; import { verifyToken } from "@/utils/jwt"; +import { refreshAccessToken } from "@/utils/authFetch"; import { ENVIRONMENT } from "@/config"; +import { requireAuthHook } from "@/hooks/requireAuthHook"; const token = Astro.cookies.get("access_token")?.value; let user = null; diff --git a/src/pages/TRip/requests.astro b/src/pages/TRip/requests.astro index 374dfab..3aaec4d 100644 --- a/src/pages/TRip/requests.astro +++ b/src/pages/TRip/requests.astro @@ -3,26 +3,9 @@ import MediaRequestForm from "@/components/TRip/MediaRequestForm" import Base from "@/layouts/Base.astro"; import Root from "@/components/AppLayout.jsx"; import { verifyToken } from "@/utils/jwt"; +import { requireAuthHook } from "@/hooks/requireAuthHook"; import { ENVIRONMENT } from "@/config"; -const token = Astro.cookies.get("access_token")?.value; -let user = null; - -try { - if (token) { - user = verifyToken(token); - if (user) { - console.log("Verified!", user); - } else { - throw Error("Authentication required"); - } - } else { - throw Error("Authentication required"); - } -} catch { - return Astro.redirect('/login' - ); -} ---
diff --git a/src/pages/login.astro b/src/pages/login.astro index 4761565..edab326 100644 --- a/src/pages/login.astro +++ b/src/pages/login.astro @@ -6,7 +6,7 @@ import Root from "@/components/AppLayout.jsx";
- +
\ No newline at end of file diff --git a/src/pages/radio.astro b/src/pages/radio.astro index ed8e0ae..b526f42 100644 --- a/src/pages/radio.astro +++ b/src/pages/radio.astro @@ -9,7 +9,5 @@ import "@styles/player.css";
-