All checks were successful
Deploy / changes (push) Successful in 5s
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m42s
Deploy Trade-In / deploy (push) Successful in 39s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / deploy (push) Successful in 1m1s
PR B (frontend) to backend RBAC #585. RouteGuard wraps app, NoAccessScreen on 403 from /me, TopNav filtered by role.
106 lines
3.2 KiB
TypeScript
106 lines
3.2 KiB
TypeScript
"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";
|
||
|
||
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}
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|