Compare commits
No commits in common. "3945060f0cccf2a5e22787a57fb69897581b20d5" and "64d3a925ef0bba3d842990adbd942f7781506595" have entirely different histories.
3945060f0c
...
64d3a925ef
4 changed files with 8 additions and 89 deletions
|
|
@ -44,11 +44,6 @@ 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
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -70,26 +65,6 @@ 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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -218,16 +193,12 @@ 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_profile_for_user, get_user_scope # noqa: E402
|
from app.core.auth import get_brand_for_user, get_user_scope # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -59,41 +59,3 @@ 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, display_name, org, email}
|
// fixture). /me returns {username, role, brand} — no display name / email, so
|
||||||
// (#2046) — known profile fields fall back to username / brand ?? role / ""
|
// name falls back to username, org to brand ?? role, email to "" (renders
|
||||||
// when absent (never invented). undefined while loading → TopNav «Гость».
|
// blank, never invented). undefined while loading → TopNav «Гость» fallback.
|
||||||
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,23 +557,14 @@ 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.display_name ?? scope.username,
|
name: scope.username,
|
||||||
org: scope.org ?? scope.brand ?? scope.role,
|
org: scope.brand ?? scope.role,
|
||||||
email: scope.email ?? "",
|
email: "",
|
||||||
initials,
|
initials: scope.username.slice(0, 2).toUpperCase(),
|
||||||
};
|
};
|
||||||
}, [me.data]);
|
}, [me.data]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,6 @@ 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