Compare commits
No commits in common. "main" and "feat/tradein-caddy-cutover" have entirely different histories.
main
...
feat/trade
3 changed files with 1 additions and 120 deletions
|
|
@ -9,19 +9,9 @@
|
||||||
* доомed 403 round-trip), но HTTPError 403 от самого списка сотрудников тоже
|
* доомed 403 round-trip), но HTTPError 403 от самого списка сотрудников тоже
|
||||||
* ловится — тот же defense-in-depth паттерн, что и `app/admin/audit/page.tsx`
|
* ловится — тот же defense-in-depth паттерн, что и `app/admin/audit/page.tsx`
|
||||||
* (прямой заход на URL не крашит страницу, даже если nav её уже прячет).
|
* (прямой заход на URL не крашит страницу, даже если nav её уже прячет).
|
||||||
*
|
|
||||||
* #2556 fix (эпик #2549, live-audit): team-роуты требуют session-cookie
|
|
||||||
* (см. team-api.ts header comment) — легаси Caddy-заголовок (`/me` 200 с
|
|
||||||
* ролью admin/manager) через него НЕ проходит. Юзер, прошедший только Caddy
|
|
||||||
* basic_auth (без входа в форму `/login`), видел здесь непонятную красную
|
|
||||||
* плашку «Не удалось выполнить запрос». Ловим именно 401 от employeesQ
|
|
||||||
* ОТДЕЛЬНО от 403 (403 = «нет прав», это ожидаемо и есть у другой роли; 401
|
|
||||||
* = «нет сессии вообще», это решается логином) и уводим на /login?next=/team
|
|
||||||
* — тот же redirect-паттерн, что RouteGuard.tsx использует для 401 от /me.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import "@/components/trade-in/trade-in.css";
|
import "@/components/trade-in/trade-in.css";
|
||||||
import { NoAccessScreen } from "@/components/auth/NoAccessScreen";
|
import { NoAccessScreen } from "@/components/auth/NoAccessScreen";
|
||||||
|
|
@ -36,11 +26,9 @@ import { teamErrorMessage, useEmployees } from "@/lib/team-api";
|
||||||
import { useMe } from "@/lib/useMe";
|
import { useMe } from "@/lib/useMe";
|
||||||
|
|
||||||
const PAGE_LIMIT = 50;
|
const PAGE_LIMIT = 50;
|
||||||
const TEAM_LOGIN_NEXT = `/login?next=${encodeURIComponent("/team")}`;
|
|
||||||
|
|
||||||
export default function TeamPage() {
|
export default function TeamPage() {
|
||||||
const meQ = useMe();
|
const meQ = useMe();
|
||||||
const router = useRouter();
|
|
||||||
// Вычисляем ДО useEmployees (не после) — иначе для employee/analyst/pilot
|
// Вычисляем ДО useEmployees (не после) — иначе для employee/analyst/pilot
|
||||||
// при прямом заходе на /team улетает обречённый GET (401/403) ещё до того,
|
// при прямом заходе на /team улетает обречённый GET (401/403) ещё до того,
|
||||||
// как ниже отрисуется role-gate. Порядок хуков не меняется — это просто
|
// как ниже отрисуется role-gate. Порядок хуков не меняется — это просто
|
||||||
|
|
@ -55,23 +43,6 @@ export default function TeamPage() {
|
||||||
const [quotaEmployee, setQuotaEmployee] = useState<Employee | null>(null);
|
const [quotaEmployee, setQuotaEmployee] = useState<Employee | null>(null);
|
||||||
const [historyEmployee, setHistoryEmployee] = useState<Employee | null>(null);
|
const [historyEmployee, setHistoryEmployee] = useState<Employee | null>(null);
|
||||||
|
|
||||||
// 401 = легаси Caddy-роль прошла role-gate выше, но team-эндпоинт требует
|
|
||||||
// session-cookie, которой у юзера нет (см. header comment). Отдельно от 403
|
|
||||||
// (та ветка — «роль есть, прав недостаточно», ниже).
|
|
||||||
const sessionRequired =
|
|
||||||
employeesQ.error instanceof HTTPError && employeesQ.error.status === 401;
|
|
||||||
|
|
||||||
// Редирект — побочный эффект (нельзя router.push во время рендера), тот же
|
|
||||||
// паттерн, что RouteGuard.tsx использует для 401 от /me. NODE_ENV-гейт
|
|
||||||
// зеркалит RouteGuard: в dev без Caddy/логина 401 — обычное дело, авто-
|
|
||||||
// редирект там мешал бы локальной разработке; вместо него — сообщение с
|
|
||||||
// кнопкой ниже.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sessionRequired) return;
|
|
||||||
if (process.env.NODE_ENV !== "production") return;
|
|
||||||
router.push(TEAM_LOGIN_NEXT);
|
|
||||||
}, [sessionRequired, router]);
|
|
||||||
|
|
||||||
// Роль ещё не загружена — не решаем ничего, RouteGuard уже отрисовал
|
// Роль ещё не загружена — не решаем ничего, RouteGuard уже отрисовал
|
||||||
// страницу (401/403 верхнего уровня он ловит сам), здесь только role-gate.
|
// страницу (401/403 верхнего уровня он ловит сам), здесь только role-gate.
|
||||||
if (meQ.isPending) {
|
if (meQ.isPending) {
|
||||||
|
|
@ -89,30 +60,6 @@ export default function TeamPage() {
|
||||||
return <NoAccessScreen variant="user" />;
|
return <NoAccessScreen variant="user" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionRequired) {
|
|
||||||
// Prod: редирект уже запущен эффектом выше — не мигаем error-текстом,
|
|
||||||
// пока навигация не завершится (тот же приём, что RouteGuard.tsx).
|
|
||||||
if (process.env.NODE_ENV === "production") return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Topbar active="team" />
|
|
||||||
<main className="page scraper-page" style={{ maxWidth: 1180 }}>
|
|
||||||
<h1 className="scraper-h1">Команда</h1>
|
|
||||||
<p className="scraper-result scraper-result--error">
|
|
||||||
Войдите в систему, чтобы управлять командой.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href={TEAM_LOGIN_NEXT}
|
|
||||||
className="team-btn-primary"
|
|
||||||
style={{ display: "inline-block", textDecoration: "none" }}
|
|
||||||
>
|
|
||||||
Войти
|
|
||||||
</a>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backend 403 регардлесс роли фронта (напр. session истекла между /me и
|
// Backend 403 регардлесс роли фронта (напр. session истекла между /me и
|
||||||
// /employees) — тот же fallback, что и в admin/audit.
|
// /employees) — тот же fallback, что и в admin/audit.
|
||||||
if (employeesQ.error instanceof HTTPError && employeesQ.error.status === 403) {
|
if (employeesQ.error instanceof HTTPError && employeesQ.error.status === 403) {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ import type {
|
||||||
TradeInEstimateInput,
|
TradeInEstimateInput,
|
||||||
} from "@/types/trade-in";
|
} from "@/types/trade-in";
|
||||||
import { HTTPError } from "@/lib/api";
|
import { HTTPError } from "@/lib/api";
|
||||||
import { isPathAllowed } from "@/lib/isPathAllowed";
|
|
||||||
import {
|
import {
|
||||||
useEstimate,
|
useEstimate,
|
||||||
useEstimateHistory,
|
useEstimateHistory,
|
||||||
|
|
@ -667,21 +666,6 @@ export default function TradeInV2Page() {
|
||||||
};
|
};
|
||||||
}, [me.data]);
|
}, [me.data]);
|
||||||
|
|
||||||
// #2556 (эпик #2549): вход в раздел «Команда» из v2-навигации — раньше
|
|
||||||
// TopNav (v2) не знал про team-дашборд вообще, он был достижим только из
|
|
||||||
// legacy Topbar.tsx (пункт в NAV_ITEMS), а `/trade-in/` редиректит на
|
|
||||||
// `/trade-in/v2` → дашборд существовал, но был недостижим кликом. Тот же
|
|
||||||
// двойной гейт, что и Topbar.NAV_ITEMS "team": isPathAllowed(scopePath) —
|
|
||||||
// DB-роль `manager` получает голый `/api/v1/team/**` в allowed_paths (см.
|
|
||||||
// DB_ROLE_PATHS, tradein-mvp/backend/app/services/auth_session.py) — ПЛЮС
|
|
||||||
// roleGate поверх, потому что legacy-роль `analyst` (auth/roles.yaml,
|
|
||||||
// paths: "/**", без явного deny на /api/v1/team) тоже прошла бы
|
|
||||||
// isPathAllowed, но не должна видеть «Команда» (scope: только admin/manager).
|
|
||||||
const showTeamNavItem =
|
|
||||||
me.data != null &&
|
|
||||||
isPathAllowed(me.data.allowed_paths, me.data.deny_paths, "/api/v1/team") &&
|
|
||||||
(me.data.role === "admin" || me.data.role === "manager");
|
|
||||||
|
|
||||||
// Analog price pins for the 01 map, projected from the real estimate. No
|
// Analog price pins for the 01 map, projected from the real estimate. No
|
||||||
// estimate → mapMarkers(null) → [] → ParamsPanel renders only the subject pin
|
// estimate → mapMarkers(null) → [] → ParamsPanel renders only the subject pin
|
||||||
// (Finding #2: never the static fixture price markers).
|
// (Finding #2: never the static fixture price markers).
|
||||||
|
|
@ -994,7 +978,6 @@ export default function TradeInV2Page() {
|
||||||
reports={reportsCount ?? 0}
|
reports={reportsCount ?? 0}
|
||||||
user={topNavUser}
|
user={topNavUser}
|
||||||
onLogout={() => logoutMutation.mutate()}
|
onLogout={() => logoutMutation.mutate()}
|
||||||
showTeamLink={showTeamNavItem}
|
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
<main
|
<main
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
import { API_BASE_URL } from "@/lib/api";
|
|
||||||
|
|
||||||
import { tokens } from "./tokens";
|
import { tokens } from "./tokens";
|
||||||
import { navLabels, version } from "./fixtures";
|
import { navLabels, version } from "./fixtures";
|
||||||
import { useSupportChat } from "./SupportChatContext";
|
import { useSupportChat } from "./SupportChatContext";
|
||||||
|
|
@ -37,12 +35,6 @@ interface TopNavProps {
|
||||||
user?: TopNavUser;
|
user?: TopNavUser;
|
||||||
// Sign out — page wires this to logout(); closes the menu first.
|
// Sign out — page wires this to logout(); closes the menu first.
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
// #2556 (эпик #2549): показывать пункт «Команда» в user-меню — вычисляется
|
|
||||||
// страницей из useMe() (роль admin/manager + isPathAllowed на /api/v1/team,
|
|
||||||
// тот же двойной гейт, что и Topbar.tsx NAV_ITEMS "team"). false/undefined
|
|
||||||
// → пункт не рендерится (роль employee/pilot/analyst/expired или /me ещё
|
|
||||||
// не загрузился).
|
|
||||||
showTeamLink?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neutral fallback when the user prop is absent (loading / unauthenticated).
|
// Neutral fallback when the user prop is absent (loading / unauthenticated).
|
||||||
|
|
@ -65,24 +57,6 @@ const menuItemStyle: CSSProperties = {
|
||||||
transition: "background .12s",
|
transition: "background .12s",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Inline Users icon (lucide-react `Users` SVG path, stroke 1.2) — для
|
|
||||||
пункта «Команда» в user-меню (#2556). Тот же inline-SVG паттерн (15x15,
|
|
||||||
tokens.muted), что и остальные иконки этого дропдауна. */
|
|
||||||
function UsersIcon() {
|
|
||||||
return (
|
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
||||||
<circle cx="5.3" cy="5" r="2.2" stroke={tokens.muted} strokeWidth="1.2" />
|
|
||||||
<path
|
|
||||||
d="M1 13c0-2.6 1.9-3.9 4.3-3.9S9.6 10.4 9.6 13"
|
|
||||||
stroke={tokens.muted}
|
|
||||||
strokeWidth="1.2"
|
|
||||||
/>
|
|
||||||
<path d="M9.3 1.8a2.1 2.1 0 0 1 0 4" stroke={tokens.muted} strokeWidth="1.2" />
|
|
||||||
<path d="M11 9.5c1.9.4 3 1.6 3 3.5" stroke={tokens.muted} strokeWidth="1.2" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Профиль / Настройки have no pages yet — render them dimmed and
|
// Профиль / Настройки have no pages yet — render them dimmed and
|
||||||
// non-interactive (no hover class, default cursor) so they read as disabled.
|
// non-interactive (no hover class, default cursor) so they read as disabled.
|
||||||
// «Помощь» used to be in this group too, then linked out to the Telegram
|
// «Помощь» used to be in this group too, then linked out to the Telegram
|
||||||
|
|
@ -100,7 +74,6 @@ export default function TopNav({
|
||||||
reports = 0,
|
reports = 0,
|
||||||
user,
|
user,
|
||||||
onLogout,
|
onLogout,
|
||||||
showTeamLink = false,
|
|
||||||
}: TopNavProps) {
|
}: TopNavProps) {
|
||||||
const [userOpen, setUserOpen] = useState(false);
|
const [userOpen, setUserOpen] = useState(false);
|
||||||
const { openChat } = useSupportChat();
|
const { openChat } = useSupportChat();
|
||||||
|
|
@ -516,28 +489,6 @@ export default function TopNav({
|
||||||
Помощь
|
Помощь
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* «Команда» (#2556, эпик #2549) — team-дашборд (`/team`) отдельный
|
|
||||||
роут, не таб SectionOverlay, поэтому это ссылка в user-меню
|
|
||||||
рядом с «Выйти», а не пункт в tnav-tab-ряду выше: v2-табы —
|
|
||||||
это разделы ОДНОЙ страницы (переключают `active`/onNavigate),
|
|
||||||
а «Команда» уводит на другой URL. Гейт (showTeamLink) считает
|
|
||||||
страница из useMe() — здесь только рендер. */}
|
|
||||||
{showTeamLink && (
|
|
||||||
<a
|
|
||||||
role="menuitem"
|
|
||||||
href={`${API_BASE_URL}/team`}
|
|
||||||
className="tnav-menuitem"
|
|
||||||
style={{
|
|
||||||
...menuItemStyle,
|
|
||||||
textDecoration: "none",
|
|
||||||
}}
|
|
||||||
onClick={() => setUserOpen(false)}
|
|
||||||
>
|
|
||||||
<UsersIcon />
|
|
||||||
Команда
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: "1px",
|
height: "1px",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue