diff --git a/furumi-node-player/client/src/App.css b/furumi-node-player/client/src/App.css index 1334e57..ca9687d 100644 --- a/furumi-node-player/client/src/App.css +++ b/furumi-node-player/client/src/App.css @@ -1,71 +1,123 @@ -.page { +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +.auth-page { min-height: 100vh; display: grid; place-items: center; padding: 24px; + background: #0a0c12; + font-family: 'Inter', system-ui, sans-serif; + color: #e2e8f0; } -.card { - width: min(520px, 100%); - border: 1px solid #d8dde6; - border-radius: 14px; - padding: 24px; - background-color: #ffffff; - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08); +/* ---------- loading spinner ---------- */ + +.auth-loading { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; } -.subtitle { - margin-top: 0; - margin-bottom: 20px; - color: #5a6475; +.spinner { + width: 36px; + height: 36px; + border: 3px solid #1f2c45; + border-top-color: #7c6af7; + border-radius: 50%; + animation: spin 0.8s linear infinite; } -.settings { - margin-bottom: 16px; - padding: 12px; - border: 1px solid #e6eaf2; - border-radius: 10px; - background: #f8fafc; +@keyframes spin { + to { transform: rotate(360deg); } } -.toggle { +.auth-loading .logo { + font-size: 1.6rem; + font-weight: 700; + color: #7c6af7; +} + +.auth-loading p { + color: #64748b; + font-size: 0.85rem; +} + +/* ---------- login card ---------- */ + +.auth-card { + width: min(380px, 100%); + background: #111520; + border: 1px solid #1f2c45; + border-radius: 16px; + padding: 2.5rem 2rem; + text-align: center; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.auth-card .logo { + font-size: 1.8rem; + font-weight: 700; + color: #7c6af7; + margin-bottom: 4px; +} + +.auth-card .subtitle { + font-size: 0.85rem; + color: #64748b; + margin-bottom: 2rem; +} + +.auth-card .btn-login { + display: block; + width: 100%; + padding: 0.75rem; + text-align: center; + background: #7c6af7; + border: none; + border-radius: 8px; + color: #fff; + font-size: 0.95rem; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: background 0.2s; +} + +.auth-card .btn-login:hover { + background: #6b58e8; +} + +.auth-card .error { + color: #f87171; + font-size: 0.85rem; + margin-bottom: 1rem; +} + +.auth-card .settings { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #1f2c45; +} + +.auth-card .toggle { display: flex; align-items: center; - gap: 10px; - color: #0f172a; - font-weight: 600; + justify-content: center; + gap: 8px; + color: #64748b; + font-size: 0.8rem; + cursor: pointer; } -.toggle input { - width: 18px; - height: 18px; -} - -.hint { - margin: 10px 0 0; - color: #5a6475; -} - -.btn { - display: inline-block; - text-decoration: none; - background: #2251ff; - color: #ffffff; - padding: 10px 16px; - border-radius: 8px; - font-weight: 600; -} - -.btn.ghost { - background: #edf1ff; - color: #1e3fc4; - margin-top: 10px; -} - -.profile p { - margin: 8px 0; -} - -.error { - color: #cc1e1e; +.auth-card .toggle input { + width: 14px; + height: 14px; + accent-color: #7c6af7; } diff --git a/furumi-node-player/client/src/App.tsx b/furumi-node-player/client/src/App.tsx index fed1fdb..16e0d5b 100644 --- a/furumi-node-player/client/src/App.tsx +++ b/furumi-node-player/client/src/App.tsx @@ -23,8 +23,6 @@ function App() { } }) - const apiBase = '' - useEffect(() => { if (runWithoutAuth) { setError(null) @@ -35,9 +33,7 @@ function App() { const loadMe = async () => { try { - const response = await fetch(`${apiBase}/auth/me`, { - credentials: 'include', - }) + const response = await fetch('/auth/me', { credentials: 'include' }) if (response.status === 401) { setUser(null) @@ -52,12 +48,9 @@ function App() { const data = await response.json() setUser(data.user ?? null) - // Fetch OIDC access token for Rust API Bearer auth if (data.user) { try { - const tokenRes = await fetch(`${apiBase}/auth/token`, { - credentials: 'include', - }) + const tokenRes = await fetch('/auth/token', { credentials: 'include' }) if (tokenRes.ok) { const tokenData = await tokenRes.json() if (tokenData.access_token) { @@ -65,7 +58,7 @@ function App() { } } } catch { - // Token fetch failed — API calls will fall back to other auth methods + // Token fetch failed } } } catch (err) { @@ -78,82 +71,60 @@ function App() { void loadMe() }, [runWithoutAuth]) - const loginUrl = `${apiBase}/auth/login` - const logoutUrl = `${apiBase}/auth/logout` + // Authenticated — render player immediately + if (!loading && (user || runWithoutAuth)) { + return + } + // Loading — show spinner (no login form flash) + if (loading) { + return ( +
+
+
Furumi
+
+

Loading...

+
+
+ ) + } + + // Not authenticated — show login return ( - <> - {!loading && (user || runWithoutAuth) ? ( - - ) : ( -
-
-

OIDC Login

-

Авторизация обрабатывается на Express сервере.

+
+
+
Furumi
+

Sign in to continue

-
- -
+ {error &&

{error}

} - {loading &&

Проверяю сессию...

} - {error &&

Ошибка: {error}

} + + Sign in with SSO + - {!loading && runWithoutAuth && ( -

- Режим без авторизации включён. Для входа отключи настройку выше. -

- )} - - {!loading && !user && ( - - Войти через OIDC - - )} - - {!loading && user && ( -
-

- ID: {user.sub} -

- {user.name && ( -

- Имя: {user.name} -

- )} - {user.email && ( -

- Email: {user.email} -

- )} - {!runWithoutAuth && ( - - Выйти - - )} -
- )} -
-
- )} - +
+ +
+
+
) }