// web/app.jsx — корневой компонент.
// Hash-routing: #/shoots, #/ingest, #/settings.
// При первом запуске сам определяет где мы (Я.Диск подключён или нет).
function App() {
// Гейт авторизации (фаза 2). Токен в localStorage → проверяем auth.Me.
const [auth, setAuth] = React.useState({ checked: false, me: null, view: "login", justRegistered: false });
React.useEffect(() => {
if (!api.auth.getToken()) { setAuth((a) => ({ ...a, checked: true })); return; }
api.auth.Me()
.then((me) => setAuth((a) => ({ ...a, checked: true, me })))
.catch(() => { api.auth.logout(); setAuth((a) => ({ ...a, checked: true, me: null })); });
}, []);
if (!auth.checked) {
return
загрузка…
;
}
if (!auth.me) {
return auth.view === "register"
? setAuth({ checked: true, me, view: "login", justRegistered: true })}
onGoLogin={() => setAuth((a) => ({ ...a, view: "login" }))} />
: setAuth({ checked: true, me, view: "login", justRegistered: false })}
onGoRegister={() => setAuth((a) => ({ ...a, view: "register" }))} />;
}
const logout = () => { api.auth.logout(); setAuth({ checked: true, me: null, view: "login", justRegistered: false }); };
return (
);
}
function AppInner({ me, justRegistered, onLogout }) {
const [routeState, _setRoute] = React.useState(() => parseHash());
const route = routeState.name;
const routeArg = routeState.arg;
const [yandex] = useAsync(() => api.yandex.Status(), []);
// setRoute(name) — простой переход, setRoute(name, arg) — с параметром.
const setRoute = React.useCallback((name, arg) => {
const hash = arg != null ? "#/" + name + "/" + arg : "#/" + name;
window.location.hash = hash;
_setRoute({ name, arg });
}, []);
// Impersonation (И3): админ «Смотреть как {логин}». setImpersonate шлёт
// заголовок X-Impersonate-User на /rpc/. mode: view (просмотр) / control.
const [imp, setImp] = React.useState(null);
const startImpersonate = (user) => { api.admin.setImpersonate(user.id); setImp({ user, mode: "view" }); setRoute("shoots"); };
const stopImpersonate = () => { api.admin.setImpersonate(0); setImp(null); setRoute("users"); };
const takeControl = () => setImp((im) => im ? { ...im, mode: "control" } : im);
// Эффективный профиль/права (И5.2): под impersonation — целевого юзера
// (auth.Me с заголовком X-Impersonate-User возвращает ЕГО права), иначе сам.
// По effMe гейтим UI — чтобы «Смотреть как» показывал реальный урезанный вид.
const [effMe, setEffMe] = React.useState(me);
React.useEffect(() => {
let alive = true;
if (imp) {
api.auth.Me().then((m) => { if (alive) setEffMe(m); }).catch(() => { if (alive) setEffMe(me); });
} else {
setEffMe(me);
}
return () => { alive = false; };
}, [imp, me]);
const can = (code) => !!(effMe && (effMe.isAdmin || (effMe.rights || []).includes(code)));
React.useEffect(() => {
const h = () => _setRoute(parseHash());
window.addEventListener("hashchange", h);
return () => window.removeEventListener("hashchange", h);
}, []);
// Когда узнали статус Я.Диска первый раз — если не подключён и route не задан явно,
// перенаправляем на /settings.
React.useEffect(() => {
if (!yandex.data) return;
if (!yandex.data.connected && route === "shoots" && !window.location.hash) {
setRoute("settings");
}
}, [yandex.data]);
// Уводим на «Мои съёмки» с недоступных роутов (напр. из устаревшего хэша
// после смены пользователя): админ-роуты — только админу, ingest — только
// тем, у кого доступ к Я.Диску.
React.useEffect(() => {
if (!effMe) return;
const adminOnly = route === "plans" || route === "users" || route === "settings";
if (adminOnly && !effMe.isAdmin) { setRoute("shoots"); return; }
if (route === "ingest" && !(effMe.isAdmin || effMe.ownYandex)) { setRoute("shoots"); return; }
if (route === "review1" && !can("r1.access")) { setRoute("shoots"); return; }
if (route === "review2" && !can("r2.access")) { setRoute("shoots"); return; }
if (route === "releases" && !can("release.access")) { setRoute("shoots"); }
}, [effMe, route]);
// Содержимое экрана.
let screen;
if (yandex.loading && route === "shoots") {
screen =
загружаем…
;
} else if (route === "settings") {
screen = ;
} else if (route === "ingest") {
screen = ;
} else if (route === "archive") {
screen = ;
} else if (route === "releases") {
screen = ;
} else if (route === "upload") {
screen = ;
} else if (route === "plans" && effMe && effMe.isAdmin) {
screen = ;
} else if (route === "users" && effMe && effMe.isAdmin) {
screen = ;
} else if (route === "series" && routeArg) {
screen = ;
} else if (route === "review1" && routeArg) {
screen = can("r1.access")
?
: ;
} else if (route === "review2" && routeArg) {
screen = can("r2.access")
?
: ;
} else if (route === "shoots") {
// Экран «подключите Яндекс» — только для тех, кто с Я.Диском работает
// (админ / «Свой Яндекс»). Обычный юзер всегда видит свои серии.
const yAccess = effMe && (effMe.isAdmin || effMe.ownYandex);
if (yAccess && yandex.data && !yandex.data.connected) {
screen = ;
} else {
screen = ;
}
} else {
screen = ;
}
return (
{imp && (
👁 Смотрю как {imp.user.login} · {imp.mode === "control" ? "режим управления — действия применяются к аккаунту пользователя" : "режим просмотра — изменения не сохраняются"}
{imp.mode === "view"
?
: ● Управление активно}