142 lines
4.0 KiB
TypeScript
142 lines
4.0 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
||
import { FurumiPlayer } from './FurumiPlayer'
|
||
import './App.css'
|
||
|
||
type UserProfile = {
|
||
sub: string
|
||
name?: string
|
||
email?: string
|
||
}
|
||
|
||
const NO_AUTH_STORAGE_KEY = 'furumiNodePlayer.runWithoutAuth'
|
||
|
||
function App() {
|
||
const [loading, setLoading] = useState(true)
|
||
const [user, setUser] = useState<UserProfile | null>(null)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [runWithoutAuth, setRunWithoutAuth] = useState(() => {
|
||
try {
|
||
return window.localStorage.getItem(NO_AUTH_STORAGE_KEY) === '1'
|
||
} catch {
|
||
return false
|
||
}
|
||
})
|
||
|
||
const apiBase = useMemo(() => import.meta.env.VITE_API_BASE_URL ?? '', [])
|
||
|
||
useEffect(() => {
|
||
if (runWithoutAuth) {
|
||
setError(null)
|
||
setUser({ sub: 'noauth', name: 'No Auth' })
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
const loadMe = async () => {
|
||
try {
|
||
const response = await fetch(`${apiBase}/api/me`, {
|
||
credentials: 'include',
|
||
})
|
||
|
||
if (response.status === 401) {
|
||
setUser(null)
|
||
return
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Request failed with status ${response.status}`)
|
||
}
|
||
|
||
const data = await response.json()
|
||
setUser(data.user ?? null)
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Failed to load session')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
void loadMe()
|
||
}, [apiBase, runWithoutAuth])
|
||
|
||
const loginUrl = `${apiBase}/api/login`
|
||
const logoutUrl = `${apiBase}/api/logout`
|
||
|
||
return (
|
||
<>
|
||
{!loading && (user || runWithoutAuth) ? (
|
||
<FurumiPlayer />
|
||
) : (
|
||
<main className="page">
|
||
<section className="card">
|
||
<h1>OIDC Login</h1>
|
||
<p className="subtitle">Авторизация обрабатывается на Express сервере.</p>
|
||
|
||
<div className="settings">
|
||
<label className="toggle">
|
||
<input
|
||
type="checkbox"
|
||
checked={runWithoutAuth}
|
||
onChange={(e) => {
|
||
const next = e.target.checked
|
||
setRunWithoutAuth(next)
|
||
try {
|
||
if (next) window.localStorage.setItem(NO_AUTH_STORAGE_KEY, '1')
|
||
else window.localStorage.removeItem(NO_AUTH_STORAGE_KEY)
|
||
} catch {
|
||
// ignore
|
||
}
|
||
setLoading(true)
|
||
setUser(null)
|
||
}}
|
||
/>
|
||
<span>Запускать без авторизации</span>
|
||
</label>
|
||
</div>
|
||
|
||
{loading && <p>Проверяю сессию...</p>}
|
||
{error && <p className="error">Ошибка: {error}</p>}
|
||
|
||
{!loading && runWithoutAuth && (
|
||
<p className="hint">
|
||
Режим без авторизации включён. Для входа отключи настройку выше.
|
||
</p>
|
||
)}
|
||
|
||
{!loading && !user && (
|
||
<a className="btn" href={loginUrl}>
|
||
Войти через OIDC
|
||
</a>
|
||
)}
|
||
|
||
{!loading && user && (
|
||
<div className="profile">
|
||
<p>
|
||
<strong>ID:</strong> {user.sub}
|
||
</p>
|
||
{user.name && (
|
||
<p>
|
||
<strong>Имя:</strong> {user.name}
|
||
</p>
|
||
)}
|
||
{user.email && (
|
||
<p>
|
||
<strong>Email:</strong> {user.email}
|
||
</p>
|
||
)}
|
||
{!runWithoutAuth && (
|
||
<a className="btn ghost" href={logoutUrl}>
|
||
Выйти
|
||
</a>
|
||
)}
|
||
</div>
|
||
)}
|
||
</section>
|
||
</main>
|
||
)}
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default App
|