gendesign/frontend/src/components/auth/NoAccessScreen.tsx
bot-backend a9dfe44f5c
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 51s
CI / openapi-codegen-check (pull_request) Successful in 1m56s
fix(tradein/auth): always-available logout button on NoAccessScreen
Locked/no-access users (403/denied path/expired session/trial ended) were
trapped: NoAccessScreen had no logout control, and «Выйти» lived only inside
UserMenu, which does not render on the no-access screen. They could not switch
accounts without clearing browser basic-auth manually.

- Extract logout() into shared `lib/logout.ts` (single source of truth).
- UserMenu now imports it instead of a local copy (behaviour unchanged).
- NoAccessScreen renders an always-available «Выйти» accent button for every
  variant.

Mirrored across both frontends (tradein-mvp/frontend + main frontend) to keep
the MIRROR invariant in sync.
2026-06-27 15:16:42 +03:00

128 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* Полноэкранный «доступа нет» — показывается в двух случаях:
* 1. /api/v1/me вернул 403 (юзер аутентифицирован Caddy basic_auth,
* но его нет в `auth/roles.yaml`). Variant: "user".
* 2. Юзер открыл URL, которого нет в его allowed_paths (или есть в
* deny_paths). Variant: "path".
*
* Tone-of-voice (см. `.claude/rules/ui-microcopy.md`): нейтральный business,
* без эмодзи, без «Ой!». Просто констатация.
*/
import { Lock } from "lucide-react";
import { logout } from "@/lib/logout";
interface NoAccessScreenProps {
/** "user" — юзер вне ролей; "path" — путь вне scope */
variant: "user" | "path";
/** Текущий путь — используется только для variant="path" в caption */
path?: string;
}
export function NoAccessScreen({ variant, path }: NoAccessScreenProps) {
const title = "Доступа нет";
const subtitle =
variant === "user"
? "Учётная запись не привязана к роли. Обратитесь к администратору GenDesign — kopylov."
: "У вашей роли нет доступа к этому разделу. Вернитесь на главную или обратитесь к администратору.";
return (
<main
role="alert"
aria-live="polite"
style={{
minHeight: "100vh",
background: "var(--bg-app)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 24,
fontFamily: "Inter, -apple-system, 'Segoe UI', system-ui, sans-serif",
}}
>
<div
style={{
background: "var(--bg-card)",
border: "1px solid var(--border-card)",
borderRadius: 12,
padding: "32px 28px",
maxWidth: 480,
width: "100%",
textAlign: "left",
}}
>
<div
style={{
width: 40,
height: 40,
background: "var(--accent-soft)",
borderRadius: 8,
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
}}
>
<Lock size={20} strokeWidth={1.5} color="var(--accent)" />
</div>
<h1
style={{
margin: "0 0 8px",
fontSize: 22,
fontWeight: 600,
color: "var(--fg-primary)",
lineHeight: 1.25,
}}
>
{title}
</h1>
<p
style={{
margin: 0,
fontSize: 14,
color: "var(--fg-secondary)",
lineHeight: 1.6,
}}
>
{subtitle}
</p>
{variant === "path" && path ? (
<p
style={{
margin: "12px 0 0",
fontSize: 12,
color: "var(--fg-tertiary)",
fontFamily: "ui-monospace, SFMono-Regular, monospace",
wordBreak: "break-all",
}}
>
{path}
</p>
) : null}
{/* Always-available logout — иначе locked/no-access юзер заперт:
UserMenu (где обычно «Выйти») на этом экране не рендерится. */}
<button
type="button"
onClick={() => logout()}
style={{
marginTop: 20,
background: "var(--accent)",
color: "#FFFFFF",
border: "none",
borderRadius: 8,
padding: "10px 16px",
fontSize: 14,
fontWeight: 500,
cursor: "pointer",
fontFamily: "inherit",
}}
>
Выйти
</button>
</div>
</main>
);
}