feat(tradein/auth): expire praktika trial access + trial-ended screen (contact Artem) #1977

Merged
lekss361 merged 1 commit from feat/tradein-praktika-trial-ended into main 2026-06-27 10:35:07 +00:00
5 changed files with 63 additions and 16 deletions

View file

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

View file

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

View file

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

View file

@ -63,6 +63,11 @@ export function RouteGuard({ children }: RouteGuardProps) {
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)) {
return <NoAccessScreen variant="path" path={absolutePath} />;
}

View file

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