feat(tradein/v2): реальный display-профиль в TopNav через /me (#2046)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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
CI Trade-In / frontend-checks (pull_request) Successful in 1m37s
CI Trade-In / backend-tests (pull_request) Successful in 2m0s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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
CI Trade-In / frontend-checks (pull_request) Successful in 1m37s
CI Trade-In / backend-tests (pull_request) Successful in 2m0s
/me теперь отдаёт display_name/org/email (kopylov -> "Копылов", остальные None). TopNav использует их вместо фабрикации username-as-name; org/email фолбэк остаётся прежним для юзеров без известного профиля.
This commit is contained in:
parent
e9e3cc7dcf
commit
dafd12d9fa
4 changed files with 89 additions and 8 deletions
|
|
@ -44,6 +44,11 @@ class UserScope(TypedDict):
|
|||
# #657 white-label: brand slug, привязанный к аккаунту. None = generic UI.
|
||||
# Фронт (useBrand.ts) применяет бренд автоматически на login (без ?brand=).
|
||||
brand: str | None
|
||||
# #2046 real profile fields для TopNav (фамилия/орг/email). None = фронт
|
||||
# фолбэкается на username/role (как раньше) — фолбэк остаётся на фронте.
|
||||
display_name: str | None
|
||||
org: str | None
|
||||
email: str | None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -65,6 +70,26 @@ def get_brand_for_user(username: str) -> str | None:
|
|||
return _USERNAME_BRAND.get(username)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Account → profile mapping (#2046 real profile fields for TopNav)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Реальные имя/организация/email пилотов для TopNav-меню (замена фабрикованного
|
||||
# "username as name"). Известные данные — только display_name для kopylov
|
||||
# (фамилия «Копылов» из комментария в caddy/users.caddy.snippet). org/email для
|
||||
# него не задокументированы нигде — умышленно НЕ выдумываем, оставляем None,
|
||||
# фронт держит текущий фолбэк (username / brand ?? role / "").
|
||||
_USERNAME_PROFILE: dict[str, dict[str, str]] = {
|
||||
"kopylov": {"display_name": "Копылов"},
|
||||
}
|
||||
|
||||
|
||||
def get_profile_for_user(username: str) -> tuple[str | None, str | None, str | None]:
|
||||
"""Return (display_name, org, email) bound to *username*, or all-None."""
|
||||
profile = _USERNAME_PROFILE.get(username, {})
|
||||
return profile.get("display_name"), profile.get("org"), profile.get("email")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# YAML loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -193,12 +218,16 @@ def get_user_scope(username: str) -> UserScope:
|
|||
config = _load_roles_config()
|
||||
role = get_role(username)
|
||||
role_def = config["roles"][role]
|
||||
display_name, org, email = get_profile_for_user(username)
|
||||
return UserScope(
|
||||
username=username,
|
||||
role=role,
|
||||
allowed_paths=list(role_def.get("paths", []) or []),
|
||||
deny_paths=list(role_def.get("deny", []) or []),
|
||||
brand=get_brand_for_user(username),
|
||||
display_name=display_name,
|
||||
org=org,
|
||||
email=email,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ sys.modules.setdefault("weasyprint", _wp_mock)
|
|||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from app.core.auth import get_brand_for_user, get_user_scope # noqa: E402
|
||||
from app.core.auth import get_brand_for_user, get_profile_for_user, get_user_scope # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -59,3 +59,41 @@ def test_user_scope_brand_none_for_praktika() -> None:
|
|||
def test_user_scope_brand_none_for_generic_user() -> None:
|
||||
scope = get_user_scope("user1")
|
||||
assert scope["brand"] is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #2046 profile fields (display_name/org/email) — TopNav real identity.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("username", "expected"),
|
||||
[
|
||||
# Единственный известный display_name — фамилия Копылова (см. комментарий
|
||||
# в caddy/users.caddy.snippet). org/email для него не задокументированы —
|
||||
# намеренно None, не выдумываем.
|
||||
("kopylov", ("Копылов", None, None)),
|
||||
("praktika", (None, None, None)),
|
||||
("admin", (None, None, None)),
|
||||
("user1", (None, None, None)),
|
||||
("unknown_user", (None, None, None)),
|
||||
],
|
||||
)
|
||||
def test_get_profile_for_user(
|
||||
username: str, expected: tuple[str | None, str | None, str | None]
|
||||
) -> None:
|
||||
assert get_profile_for_user(username) == expected
|
||||
|
||||
|
||||
def test_user_scope_display_name_kopylov() -> None:
|
||||
scope = get_user_scope("kopylov")
|
||||
assert scope["display_name"] == "Копылов"
|
||||
assert scope["org"] is None
|
||||
assert scope["email"] is None
|
||||
|
||||
|
||||
def test_user_scope_profile_none_for_generic_user() -> None:
|
||||
scope = get_user_scope("user1")
|
||||
assert scope["display_name"] is None
|
||||
assert scope["org"] is None
|
||||
assert scope["email"] is None
|
||||
|
|
|
|||
|
|
@ -439,9 +439,9 @@ export default function TradeInV2Page() {
|
|||
const history = useEstimateHistory();
|
||||
const quota = useQuota();
|
||||
// Real logged-in identity for the TopNav user menu (replaces the design
|
||||
// fixture). /me returns {username, role, brand} — no display name / email, so
|
||||
// name falls back to username, org to brand ?? role, email to "" (renders
|
||||
// blank, never invented). undefined while loading → TopNav «Гость» fallback.
|
||||
// fixture). /me returns {username, role, brand, display_name, org, email}
|
||||
// (#2046) — known profile fields fall back to username / brand ?? role / ""
|
||||
// when absent (never invented). undefined while loading → TopNav «Гость».
|
||||
const me = useMe();
|
||||
|
||||
// Dashboard sub-hooks — each resolves independently; failure degrades its
|
||||
|
|
@ -557,14 +557,23 @@ export default function TradeInV2Page() {
|
|||
|
||||
// Real user for the TopNav menu. undefined until /me resolves → TopNav shows
|
||||
// its neutral «Гость» fallback (never the old "Андрей Петров / Брусника").
|
||||
// #2046: display_name/org/email — реальный профиль, если backend его знает
|
||||
// (см. app.core.auth._USERNAME_PROFILE); иначе фолбэк на username/brand?role/"".
|
||||
const topNavUser = useMemo(() => {
|
||||
const scope = me.data;
|
||||
if (!scope) return undefined;
|
||||
const nameParts = scope.display_name?.trim().split(/\s+/).filter(Boolean) ?? [];
|
||||
const initials =
|
||||
nameParts.length >= 2
|
||||
? (nameParts[0][0] + nameParts[1][0]).toUpperCase()
|
||||
: nameParts.length === 1
|
||||
? nameParts[0].slice(0, 2).toUpperCase()
|
||||
: scope.username.slice(0, 2).toUpperCase();
|
||||
return {
|
||||
name: scope.username,
|
||||
org: scope.brand ?? scope.role,
|
||||
email: "",
|
||||
initials: scope.username.slice(0, 2).toUpperCase(),
|
||||
name: scope.display_name ?? scope.username,
|
||||
org: scope.org ?? scope.brand ?? scope.role,
|
||||
email: scope.email ?? "",
|
||||
initials,
|
||||
};
|
||||
}, [me.data]);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ export interface UserScope {
|
|||
// #657 brand-by-account: slug бренда, привязанный к аккаунту (praktika→"praktika").
|
||||
// null/undefined = generic UI. useBrand применяет его автоматически на login.
|
||||
brand?: string | null;
|
||||
// #2046 real profile fields для TopNav. undefined/null = данных нет, фронт
|
||||
// фолбэкается на username / brand ?? role / "" (как раньше).
|
||||
display_name?: string | null;
|
||||
org?: string | null;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export const ME_QUERY_KEY = ["auth", "me"] as const;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue