feat(tradein/auth): expire praktika trial access + trial-ended screen (contact Artem)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped

praktika pilot→expired role in roles.yaml (paths:[], deny:/**); RouteGuard
short-circuits on role=expired with a dedicated trial-ended screen (not the
generic path-deny path). NoAccessScreen gains variant="trial" with title
«Пробный доступ закончился» and a Telegram link to @ArtemKopylov87. Backend
Role Literal and frontend Role type extended to include "expired". kopylov
(pilot) unaffected.
This commit is contained in:
bot-backend 2026-06-27 13:30:28 +03:00
parent b60df61f6a
commit 703e411a17
5 changed files with 63 additions and 16 deletions

View file

@ -54,6 +54,13 @@ roles:
- "/admin/**" - "/admin/**"
- "/api/v1/admin/**" - "/api/v1/admin/**"
- "/trade-in/api/v1/admin/**" - "/trade-in/api/v1/admin/**"
expired:
# Пробный доступ закончился — нет доступа ни к чему. Аккаунт остаётся в
# caddy/users.caddy.snippet (basic_auth), чтобы дойти до фронта и увидеть
# сообщение; /me отдаёт role=expired → RouteGuard рендерит trial-экран.
paths: []
deny:
- "/**"
# NB: реальные analyst-логины ДОЛЖНЫ быть добавлены и в # NB: реальные analyst-логины ДОЛЖНЫ быть добавлены и в
# caddy/users.caddy.snippet (Caddy basic_auth) силами devops — иначе Caddy не # caddy/users.caddy.snippet (Caddy basic_auth) силами devops — иначе Caddy не
@ -72,7 +79,7 @@ users:
user8: pilot user8: pilot
user9: pilot user9: pilot
user10: pilot user10: pilot
praktika: pilot # пилот-аккаунт агентства «Практика» 2026-05-29 praktika: expired # пробный доступ закончился 2026-06-27 — см. NoAccessScreen variant="trial"
admintest: admin # temp QA 2026-05-26 admintest: admin # temp QA 2026-05-26
pilottest: pilot # temp QA 2026-05-26 pilottest: pilot # temp QA 2026-05-26
analysttest: analyst # temp QA 2026-06-07 (#962) analysttest: analyst # temp QA 2026-06-07 (#962)

View file

@ -31,7 +31,7 @@ import yaml
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Role = Literal["admin", "pilot", "analyst"] Role = Literal["admin", "pilot", "analyst", "expired"]
class UserScope(TypedDict): class UserScope(TypedDict):

View file

@ -10,17 +10,26 @@
*/ */
interface NoAccessScreenProps { interface NoAccessScreenProps {
variant: "user" | "path" | "session"; variant: "user" | "path" | "session" | "trial";
path?: string; path?: string;
} }
export function NoAccessScreen({ variant, path }: NoAccessScreenProps) { export function NoAccessScreen({ variant, path }: NoAccessScreenProps) {
const title =
variant === "session"
? "Сессия истекла"
: variant === "trial"
? "Пробный доступ закончился"
: "Доступа нет";
const subtitle = const subtitle =
variant === "session" variant === "session"
? "Сессия истекла или вы не авторизованы. Обновите страницу, чтобы войти снова." ? "Сессия истекла или вы не авторизованы. Обновите страницу, чтобы войти снова."
: variant === "user" : variant === "user"
? "Учётная запись не привязана к роли. Обратитесь к администратору GenDesign — kopylov." ? "Учётная запись не привязана к роли. Обратитесь к администратору GenDesign — kopylov."
: "У вашей роли нет доступа к этому разделу. Вернитесь на главную или обратитесь к администратору."; : variant === "path"
? "У вашей роли нет доступа к этому разделу. Вернитесь на главную или обратитесь к администратору."
: null;
return ( return (
<main <main
@ -56,18 +65,44 @@ export function NoAccessScreen({ variant, path }: NoAccessScreenProps) {
lineHeight: 1.25, lineHeight: 1.25,
}} }}
> >
{variant === "session" ? "Сессия истекла" : "Доступа нет"} {title}
</h1> </h1>
<p {variant === "trial" ? (
style={{ <p
margin: 0, style={{
fontSize: 14, margin: 0,
color: "var(--fg-secondary)", fontSize: 14,
lineHeight: 1.6, color: "var(--fg-secondary)",
}} lineHeight: 1.6,
> }}
{subtitle} >
</p> Пробный доступ к сервису «Мера» закончился. Чтобы продлить доступ
или получить полную версию напишите Артёму Копылову:{" "}
<a
href="https://t.me/ArtemKopylov87"
target="_blank"
rel="noreferrer"
style={{
color: "var(--accent)",
textDecoration: "underline",
}}
>
@ArtemKopylov87
</a>
.
</p>
) : (
<p
style={{
margin: 0,
fontSize: 14,
color: "var(--fg-secondary)",
lineHeight: 1.6,
}}
>
{subtitle}
</p>
)}
{variant === "path" && path ? ( {variant === "path" && path ? (
<p <p
style={{ style={{

View file

@ -63,6 +63,11 @@ export function RouteGuard({ children }: RouteGuardProps) {
if (!data) return null; if (!data) return null;
// Пробный доступ закончился (#praktika): role=expired → спец-экран, а не generic path-deny.
if (data.role === "expired") {
return <NoAccessScreen variant="trial" />;
}
if (!isPathAllowed(data.allowed_paths, data.deny_paths, absolutePath)) { if (!isPathAllowed(data.allowed_paths, data.deny_paths, absolutePath)) {
return <NoAccessScreen variant="path" path={absolutePath} />; return <NoAccessScreen variant="path" path={absolutePath} />;
} }

View file

@ -14,7 +14,7 @@ import { useQuery } from "@tanstack/react-query";
import { apiFetchWithStatus, HTTPError } from "@/lib/api"; import { apiFetchWithStatus, HTTPError } from "@/lib/api";
export type Role = "admin" | "pilot"; export type Role = "admin" | "pilot" | "expired";
export interface UserScope { export interface UserScope {
username: string; username: string;