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.
|
# #657 white-label: brand slug, привязанный к аккаунту. None = generic UI.
|
||||||
# Фронт (useBrand.ts) применяет бренд автоматически на login (без ?brand=).
|
# Фронт (useBrand.ts) применяет бренд автоматически на login (без ?brand=).
|
||||||
brand: str | None
|
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)
|
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
|
# YAML loading
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -193,12 +218,16 @@ def get_user_scope(username: str) -> UserScope:
|
||||||
config = _load_roles_config()
|
config = _load_roles_config()
|
||||||
role = get_role(username)
|
role = get_role(username)
|
||||||
role_def = config["roles"][role]
|
role_def = config["roles"][role]
|
||||||
|
display_name, org, email = get_profile_for_user(username)
|
||||||
return UserScope(
|
return UserScope(
|
||||||
username=username,
|
username=username,
|
||||||
role=role,
|
role=role,
|
||||||
allowed_paths=list(role_def.get("paths", []) or []),
|
allowed_paths=list(role_def.get("paths", []) or []),
|
||||||
deny_paths=list(role_def.get("deny", []) or []),
|
deny_paths=list(role_def.get("deny", []) or []),
|
||||||
brand=get_brand_for_user(username),
|
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
|
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(
|
@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:
|
def test_user_scope_brand_none_for_generic_user() -> None:
|
||||||
scope = get_user_scope("user1")
|
scope = get_user_scope("user1")
|
||||||
assert scope["brand"] is None
|
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 history = useEstimateHistory();
|
||||||
const quota = useQuota();
|
const quota = useQuota();
|
||||||
// Real logged-in identity for the TopNav user menu (replaces the design
|
// Real logged-in identity for the TopNav user menu (replaces the design
|
||||||
// fixture). /me returns {username, role, brand} — no display name / email, so
|
// fixture). /me returns {username, role, brand, display_name, org, email}
|
||||||
// name falls back to username, org to brand ?? role, email to "" (renders
|
// (#2046) — known profile fields fall back to username / brand ?? role / ""
|
||||||
// blank, never invented). undefined while loading → TopNav «Гость» fallback.
|
// when absent (never invented). undefined while loading → TopNav «Гость».
|
||||||
const me = useMe();
|
const me = useMe();
|
||||||
|
|
||||||
// Dashboard sub-hooks — each resolves independently; failure degrades its
|
// 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
|
// Real user for the TopNav menu. undefined until /me resolves → TopNav shows
|
||||||
// its neutral «Гость» fallback (never the old "Андрей Петров / Брусника").
|
// its neutral «Гость» fallback (never the old "Андрей Петров / Брусника").
|
||||||
|
// #2046: display_name/org/email — реальный профиль, если backend его знает
|
||||||
|
// (см. app.core.auth._USERNAME_PROFILE); иначе фолбэк на username/brand?role/"".
|
||||||
const topNavUser = useMemo(() => {
|
const topNavUser = useMemo(() => {
|
||||||
const scope = me.data;
|
const scope = me.data;
|
||||||
if (!scope) return undefined;
|
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 {
|
return {
|
||||||
name: scope.username,
|
name: scope.display_name ?? scope.username,
|
||||||
org: scope.brand ?? scope.role,
|
org: scope.org ?? scope.brand ?? scope.role,
|
||||||
email: "",
|
email: scope.email ?? "",
|
||||||
initials: scope.username.slice(0, 2).toUpperCase(),
|
initials,
|
||||||
};
|
};
|
||||||
}, [me.data]);
|
}, [me.data]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,11 @@ export interface UserScope {
|
||||||
// #657 brand-by-account: slug бренда, привязанный к аккаунту (praktika→"praktika").
|
// #657 brand-by-account: slug бренда, привязанный к аккаунту (praktika→"praktika").
|
||||||
// null/undefined = generic UI. useBrand применяет его автоматически на login.
|
// null/undefined = generic UI. useBrand применяет его автоматически на login.
|
||||||
brand?: string | null;
|
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;
|
export const ME_QUERY_KEY = ["auth", "me"] as const;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue