diff --git a/external-admin/index.html b/external-admin/index.html
index a83fee8..61d85dd 100644
--- a/external-admin/index.html
+++ b/external-admin/index.html
@@ -4,7 +4,7 @@
-
Fami
+ External Admin
diff --git a/external-admin/src/i18n/ExternalI18nProvider.test.jsx b/external-admin/src/i18n/ExternalI18nProvider.test.jsx
index 86af77f..7a30b33 100644
--- a/external-admin/src/i18n/ExternalI18nProvider.test.jsx
+++ b/external-admin/src/i18n/ExternalI18nProvider.test.jsx
@@ -16,7 +16,7 @@ describe("ExternalI18nProvider", () => {
render();
expect(screen.getByTestId("locale")).toHaveTextContent("en");
- expect(screen.getByTestId("title")).toHaveTextContent("Fami");
+ expect(screen.getByTestId("title")).toHaveTextContent("External Admin");
expect(document.documentElement).toHaveAttribute("lang", "en");
expect(document.documentElement).toHaveAttribute("dir", "ltr");
});
@@ -26,25 +26,25 @@ describe("ExternalI18nProvider", () => {
const firstRender = render();
await user.click(screen.getByRole("button", { name: "ar" }));
- expect(screen.getByTestId("title")).toHaveTextContent("Fami");
+ expect(screen.getByTestId("title")).toHaveTextContent("الإدارة الخارجية");
expect(document.documentElement).toHaveAttribute("lang", "ar");
expect(document.documentElement).toHaveAttribute("dir", "rtl");
expect(document.body).toHaveAttribute("dir", "rtl");
expect(localStorage.getItem(EXTERNAL_LOCALE_STORAGE_KEY)).toBe("ar");
await user.click(screen.getByRole("button", { name: "tr" }));
- expect(screen.getByTestId("title")).toHaveTextContent("Fami");
+ expect(screen.getByTestId("title")).toHaveTextContent("Harici Yönetim");
expect(document.documentElement).toHaveAttribute("lang", "tr");
expect(document.documentElement).toHaveAttribute("dir", "ltr");
await user.click(screen.getByRole("button", { name: "zh-CN" }));
- expect(screen.getByTestId("title")).toHaveTextContent("Fami");
+ expect(screen.getByTestId("title")).toHaveTextContent("外管后台");
expect(localStorage.getItem(EXTERNAL_LOCALE_STORAGE_KEY)).toBe("zh-CN");
firstRender.unmount();
render();
expect(screen.getByTestId("locale")).toHaveTextContent("zh-CN");
- expect(screen.getByTestId("title")).toHaveTextContent("Fami");
+ expect(screen.getByTestId("title")).toHaveTextContent("外管后台");
});
});
diff --git a/external-admin/src/i18n/messages.js b/external-admin/src/i18n/messages.js
index cb399f3..71b14ec 100644
--- a/external-admin/src/i18n/messages.js
+++ b/external-admin/src/i18n/messages.js
@@ -10,8 +10,8 @@ export const EXTERNAL_LANGUAGE_OPTIONS = Object.freeze([
]);
const en = {
- "app.title": "Fami",
- "app.shortTitle": "Fami",
+ "app.title": "External Admin",
+ "app.shortTitle": "External Admin",
"language.label": "Language",
"common.actions": "Actions",
"common.active": "Active",
@@ -321,8 +321,8 @@ const en = {
};
const zhCN = {
- "app.title": "Fami",
- "app.shortTitle": "Fami",
+ "app.title": "外管后台",
+ "app.shortTitle": "外管后台",
"language.label": "语言",
"common.actions": "操作", "common.active": "正常", "common.all": "全部", "common.allStatuses": "全部状态", "common.banned": "已封禁", "common.cancel": "取消", "common.confirm": "确认", "common.create": "创建", "common.disabled": "禁用", "common.edit": "编辑", "common.enabled": "启用", "common.expired": "过期", "common.loading": "正在加载", "common.noData": "当前无数据", "common.normal": "正常", "common.query": "查询", "common.reason": "原因", "common.reload": "重新加载", "common.retry": "重试", "common.save": "保存", "common.status": "状态", "common.user": "用户",
"auth.account": "外管账号", "auth.changePassword": "修改外管密码", "auth.checkingSession": "正在校验外管会话", "auth.confirmNewPassword": "确认新密码", "auth.currentPassword": "当前密码", "auth.fillAccountPassword": "请填写账号和密码", "auth.firstLoginChange": "首次登录必须修改密码", "auth.hidePassword": "隐藏密码", "auth.invalidCredentials": "账号或密码错误", "auth.login": "登录", "auth.loginFailed": "登录失败", "auth.logout": "退出登录", "auth.newPassword": "新密码", "auth.noPermission": "当前外管账号没有访问此功能的权限。", "auth.password": "密码", "auth.passwordChangedFailed": "密码修改失败", "auth.passwordMismatch": "两次输入的新密码不一致", "auth.passwordMin": "至少 8 个字符", "auth.passwordMinError": "新密码至少 8 个字符", "auth.passwordWhitespace": "新密码不能为空或全为空白字符", "auth.recheck": "重新校验", "auth.returnOverview": "返回权限总览", "auth.savePassword": "保存密码", "auth.sessionFailed": "会话校验失败", "auth.showPassword": "显示密码",
@@ -345,7 +345,7 @@ const zhCN = {
const ar = {
...en,
- "app.title": "Fami", "app.shortTitle": "Fami", "language.label": "اللغة",
+ "app.title": "الإدارة الخارجية", "app.shortTitle": "الإدارة الخارجية", "language.label": "اللغة",
"common.actions": "الإجراءات", "common.active": "نشط", "common.all": "الكل", "common.allStatuses": "كل الحالات", "common.banned": "محظور", "common.cancel": "إلغاء", "common.confirm": "تأكيد", "common.create": "إنشاء", "common.disabled": "معطّل", "common.edit": "تعديل", "common.enabled": "مفعّل", "common.expired": "منتهي", "common.loading": "جارٍ التحميل", "common.noData": "لا توجد بيانات", "common.normal": "طبيعي", "common.query": "بحث", "common.reason": "السبب", "common.reload": "إعادة التحميل", "common.retry": "إعادة المحاولة", "common.save": "حفظ", "common.status": "الحالة", "common.user": "المستخدم",
"auth.account": "حساب الإدارة الخارجية", "auth.changePassword": "تغيير كلمة مرور الإدارة الخارجية", "auth.checkingSession": "جارٍ التحقق من الجلسة", "auth.confirmNewPassword": "تأكيد كلمة المرور الجديدة", "auth.currentPassword": "كلمة المرور الحالية", "auth.fillAccountPassword": "أدخل الحساب وكلمة المرور", "auth.firstLoginChange": "يجب تغيير كلمة المرور عند تسجيل الدخول لأول مرة", "auth.hidePassword": "إخفاء كلمة المرور", "auth.invalidCredentials": "الحساب أو كلمة المرور غير صحيحة", "auth.login": "تسجيل الدخول", "auth.loginFailed": "فشل تسجيل الدخول", "auth.logout": "تسجيل الخروج", "auth.newPassword": "كلمة المرور الجديدة", "auth.noPermission": "لا يملك هذا الحساب صلاحية الوصول إلى هذه الميزة.", "auth.password": "كلمة المرور", "auth.passwordChangedFailed": "فشل تغيير كلمة المرور", "auth.passwordMismatch": "كلمتا المرور الجديدتان غير متطابقتين", "auth.passwordMin": "8 أحرف على الأقل", "auth.passwordMinError": "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل", "auth.passwordWhitespace": "لا يمكن أن تكون كلمة المرور الجديدة فارغة أو مسافات فقط", "auth.recheck": "إعادة التحقق", "auth.returnOverview": "العودة إلى ملخص الصلاحيات", "auth.savePassword": "حفظ كلمة المرور", "auth.sessionFailed": "فشل التحقق من الجلسة", "auth.showPassword": "إظهار كلمة المرور",
"nav.expand": "فتح القائمة", "nav.overview": "ملخص الصلاحيات", "nav.users": "إدارة المستخدمين", "nav.bans": "إدارة الحظر", "nav.hosts": "المضيفون", "nav.agencies": "الوكالات", "nav.bds": "قائمة BD", "nav.bdManagers": "مديرو BD", "nav.superAdmins": "المشرفون العامون", "nav.rooms": "إدارة الغرف", "nav.grants": "الموارد والمعرّفات المميزة", "nav.banners": "إدارة اللافتات", "nav.team": "فريقي",
@@ -367,7 +367,7 @@ const ar = {
const tr = {
...en,
- "app.title": "Fami", "app.shortTitle": "Fami", "language.label": "Dil",
+ "app.title": "Harici Yönetim", "app.shortTitle": "Harici Yönetim", "language.label": "Dil",
"common.actions": "İşlemler", "common.active": "Aktif", "common.all": "Tümü", "common.allStatuses": "Tüm durumlar", "common.banned": "Yasaklı", "common.cancel": "İptal", "common.confirm": "Onayla", "common.create": "Oluştur", "common.disabled": "Devre dışı", "common.edit": "Düzenle", "common.enabled": "Etkin", "common.expired": "Süresi dolmuş", "common.loading": "Yükleniyor", "common.noData": "Veri yok", "common.normal": "Normal", "common.query": "Ara", "common.reason": "Neden", "common.reload": "Yeniden yükle", "common.retry": "Tekrar dene", "common.save": "Kaydet", "common.status": "Durum", "common.user": "Kullanıcı",
"auth.account": "Harici yönetim hesabı", "auth.changePassword": "Harici yönetim parolasını değiştir", "auth.checkingSession": "Oturum kontrol ediliyor", "auth.confirmNewPassword": "Yeni parolayı doğrula", "auth.currentPassword": "Mevcut parola", "auth.fillAccountPassword": "Hesabınızı ve parolanızı girin", "auth.firstLoginChange": "İlk girişte parolanızı değiştirmeniz gerekir", "auth.hidePassword": "Parolayı gizle", "auth.invalidCredentials": "Hesap veya parola hatalı", "auth.login": "Giriş yap", "auth.loginFailed": "Giriş başarısız", "auth.logout": "Çıkış yap", "auth.newPassword": "Yeni parola", "auth.noPermission": "Bu harici yönetim hesabının bu özelliğe erişimi yok.", "auth.password": "Parola", "auth.passwordChangedFailed": "Parola değiştirilemedi", "auth.passwordMismatch": "Yeni parolalar eşleşmiyor", "auth.passwordMin": "En az 8 karakter", "auth.passwordMinError": "Yeni parola en az 8 karakter olmalıdır", "auth.passwordWhitespace": "Yeni parola boş veya yalnızca boşluk olamaz", "auth.recheck": "Tekrar kontrol et", "auth.returnOverview": "Yetki özetine dön", "auth.savePassword": "Parolayı kaydet", "auth.sessionFailed": "Oturum doğrulanamadı", "auth.showPassword": "Parolayı göster",
"nav.expand": "Menüyü aç", "nav.overview": "Yetki özeti", "nav.users": "Kullanıcı yönetimi", "nav.bans": "Yasak yönetimi", "nav.hosts": "Yayıncılar", "nav.agencies": "Ajanslar", "nav.bds": "BD listesi", "nav.bdManagers": "BD Yöneticileri", "nav.superAdmins": "Süper Yöneticiler", "nav.rooms": "Oda yönetimi", "nav.grants": "Kaynaklar ve özel ID'ler", "nav.banners": "Banner yönetimi", "nav.team": "Ekibim",
diff --git a/external-admin/src/i18n/messages.test.js b/external-admin/src/i18n/messages.test.js
index 08ee1cf..19c7cf1 100644
--- a/external-admin/src/i18n/messages.test.js
+++ b/external-admin/src/i18n/messages.test.js
@@ -13,10 +13,12 @@ describe("external admin translations", () => {
test("uses native language names and Arabic RTL only", () => {
expect(EXTERNAL_LANGUAGE_OPTIONS.map((item) => item.label)).toEqual(["English", "简体中文", "العربية", "Türkçe"]);
- for (const locale of ["en", "zh-CN", "ar", "tr"]) {
- expect(externalMessage(locale, "app.title")).toBe("Fami");
- expect(externalMessage(locale, "app.shortTitle")).toBe("Fami");
- }
+ expect(["en", "zh-CN", "ar", "tr"].map((locale) => externalMessage(locale, "app.title"))).toEqual([
+ "External Admin", "外管后台", "الإدارة الخارجية", "Harici Yönetim"
+ ]);
+ expect(["en", "zh-CN", "ar", "tr"].map((locale) => externalMessage(locale, "app.shortTitle"))).toEqual([
+ "External Admin", "外管后台", "الإدارة الخارجية", "Harici Yönetim"
+ ]);
expect(externalDirection("ar")).toBe("rtl");
expect(externalDirection("en")).toBe("ltr");
expect(externalDirection("zh-CN")).toBe("ltr");
diff --git a/external-admin/src/layout/ExternalAdminLayout.jsx b/external-admin/src/layout/ExternalAdminLayout.jsx
index 5354b28..aedfaf5 100644
--- a/external-admin/src/layout/ExternalAdminLayout.jsx
+++ b/external-admin/src/layout/ExternalAdminLayout.jsx
@@ -27,13 +27,14 @@ import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
-import { useEffect, useMemo, useState } from "react";
+import { useMemo, useState } from "react";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
import { useExternalAuth } from "../auth/ExternalAuthProvider.jsx";
import { hasAnyExternalCapability } from "../config/capabilities.js";
import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx";
import { LanguageSwitcher } from "../i18n/LanguageSwitcher.jsx";
+import { useExternalSessionBrand } from "../shared/useExternalSessionBrand.js";
const drawerWidth = 248;
const navigation = [
@@ -64,19 +65,7 @@ export function ExternalAdminLayout() {
[session]
);
const current = [...visibleNavigation].reverse().find((item) => location.pathname.startsWith(item.path));
-
- useEffect(() => {
- // Login resolves the account's App server-side. Both the visible brand and browser metadata must use that apps row rather than a bundled image or client-selected tenant.
- const appName = String(session?.appName || session?.appCode || t("app.title")).trim();
- const logoUrl = String(session?.logoUrl || "").trim();
- globalThis.document.title = appName;
- syncBrandFavicon(logoUrl);
- return () => {
- // Leaving an authenticated App session must not leak its brand into the next account's login screen.
- globalThis.document.title = t("app.title");
- syncBrandFavicon("");
- };
- }, [session?.appCode, session?.appName, session?.logoUrl, t]);
+ useExternalSessionBrand(session, t("app.title"));
const handleLogout = async () => {
await logout();
@@ -157,19 +146,3 @@ export function ExternalAdminLayout() {
);
}
-
-function syncBrandFavicon(logoUrl) {
- const selector = 'link[data-external-app-favicon="true"]';
- const current = globalThis.document?.head?.querySelector(selector);
- if (!logoUrl) {
- current?.remove();
- return;
- }
- const link = current || globalThis.document.createElement("link");
- link.dataset.externalAppFavicon = "true";
- link.rel = "icon";
- link.href = logoUrl;
- if (!current) {
- globalThis.document.head.append(link);
- }
-}
diff --git a/external-admin/src/layout/ExternalAdminLayout.test.jsx b/external-admin/src/layout/ExternalAdminLayout.test.jsx
index 6f158b9..7419cad 100644
--- a/external-admin/src/layout/ExternalAdminLayout.test.jsx
+++ b/external-admin/src/layout/ExternalAdminLayout.test.jsx
@@ -69,7 +69,7 @@ describe("ExternalAdminLayout RTL", () => {
expect(screen.getByRole("combobox", { name: "اللغة" })).toHaveTextContent("العربية");
view.unmount();
- expect(document.title).toBe("Fami");
+ expect(document.title).toBe("الإدارة الخارجية");
expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument();
});
@@ -89,7 +89,7 @@ describe("ExternalAdminLayout RTL", () => {
expect(document.head.querySelector('link[data-external-app-favicon="true"]')).toHaveAttribute("href", "https://media.example.com/apps/lalu.png");
view.unmount();
- expect(document.title).toBe("Fami");
+ expect(document.title).toBe("الإدارة الخارجية");
expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument();
});
});
diff --git a/external-admin/src/pages/ChangePasswordPage.jsx b/external-admin/src/pages/ChangePasswordPage.jsx
index ca8c517..02b533f 100644
--- a/external-admin/src/pages/ChangePasswordPage.jsx
+++ b/external-admin/src/pages/ChangePasswordPage.jsx
@@ -1,6 +1,4 @@
-import KeyOutlined from "@mui/icons-material/KeyOutlined";
import Alert from "@mui/material/Alert";
-import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
@@ -10,10 +8,12 @@ import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
+import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
import { useExternalAuth } from "../auth/ExternalAuthProvider.jsx";
import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx";
import { LanguageSwitcher } from "../i18n/LanguageSwitcher.jsx";
import { translateExternalError } from "../i18n/errors.js";
+import { useExternalSessionBrand } from "../shared/useExternalSessionBrand.js";
export default function ChangePasswordPage() {
const { changePassword, logout, session } = useExternalAuth();
@@ -22,6 +22,7 @@ export default function ChangePasswordPage() {
const [error, setError] = useState("");
const [submitting, setSubmitting] = useState(false);
const navigate = useNavigate();
+ useExternalSessionBrand(session, t("app.title"));
const submit = async (event) => {
event.preventDefault();
@@ -59,7 +60,18 @@ export default function ChangePasswordPage() {
- {t("auth.changePassword")}{session?.passwordChangeRequired ? t("auth.firstLoginChange") : session?.account}
+
+
+ {t("auth.changePassword")}
+ {session?.passwordChangeRequired ? t("auth.firstLoginChange") : session?.account}
+
{error ? {error} : null}
setForm({ ...form, oldPassword: event.target.value })} />
diff --git a/external-admin/src/pages/ChangePasswordPage.test.jsx b/external-admin/src/pages/ChangePasswordPage.test.jsx
index 51351d7..c1f6e4f 100644
--- a/external-admin/src/pages/ChangePasswordPage.test.jsx
+++ b/external-admin/src/pages/ChangePasswordPage.test.jsx
@@ -7,7 +7,13 @@ import ChangePasswordPage from "./ChangePasswordPage.jsx";
const auth = vi.hoisted(() => ({
changePassword: vi.fn(),
logout: vi.fn(),
- session: { account: "fami-manager", passwordChangeRequired: true }
+ session: {
+ account: "fami-manager",
+ appCode: "fami",
+ appName: "Fami",
+ logoUrl: "https://media.example.com/apps/fami.png",
+ passwordChangeRequired: true
+ }
}));
vi.mock("../auth/ExternalAuthProvider.jsx", () => ({ useExternalAuth: () => auth }));
@@ -16,6 +22,29 @@ describe("ChangePasswordPage", () => {
beforeEach(() => {
auth.changePassword.mockReset().mockResolvedValue({});
auth.logout.mockReset().mockResolvedValue(undefined);
+ auth.session = {
+ account: "fami-manager",
+ appCode: "fami",
+ appName: "Fami",
+ logoUrl: "https://media.example.com/apps/fami.png",
+ passwordChangeRequired: true
+ };
+ });
+
+ test("shows the App brand resolved by the authenticated session", () => {
+ auth.session = {
+ ...auth.session,
+ account: "lalu-manager",
+ appCode: "lalu",
+ appName: "Lalu",
+ logoUrl: "https://media.example.com/apps/lalu.png"
+ };
+ const view = render();
+
+ expect(screen.getByText("Lalu")).toBeInTheDocument();
+ expect(view.container.querySelector("img")).toHaveAttribute("src", "https://media.example.com/apps/lalu.png");
+ expect(document.title).toBe("Lalu");
+ expect(document.head.querySelector('link[data-external-app-favicon="true"]')).toHaveAttribute("href", "https://media.example.com/apps/lalu.png");
});
test("blocks a whitespace-only new password", async () => {
diff --git a/external-admin/src/pages/LoginPage.jsx b/external-admin/src/pages/LoginPage.jsx
index c9a2747..963cc02 100644
--- a/external-admin/src/pages/LoginPage.jsx
+++ b/external-admin/src/pages/LoginPage.jsx
@@ -28,6 +28,12 @@ export default function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
+ useEffect(() => {
+ // 登录成功前没有可信 App 上下文;使用通用标题并清掉上一个会话的图标,避免泄露或误导用户所属 App。
+ globalThis.document.title = t("app.title");
+ globalThis.document.head.querySelector('link[data-external-app-favicon="true"]')?.remove();
+ }, [t]);
+
useEffect(() => {
if (!initializing && session) {
const destination = session.passwordChangeRequired ? "/change-password" : location.state?.from?.pathname || "/overview";
diff --git a/external-admin/src/pages/LoginPage.test.jsx b/external-admin/src/pages/LoginPage.test.jsx
index eb4ed36..000df23 100644
--- a/external-admin/src/pages/LoginPage.test.jsx
+++ b/external-admin/src/pages/LoginPage.test.jsx
@@ -18,12 +18,22 @@ describe("LoginPage", () => {
test("defaults to English, has no App field and submits only account and password", async () => {
const user = userEvent.setup();
+ const staleFavicon = document.createElement("link");
+ staleFavicon.dataset.externalAppFavicon = "true";
+ staleFavicon.rel = "icon";
+ staleFavicon.href = "https://media.example.com/apps/lalu.png";
+ document.head.append(staleFavicon);
+ document.title = "Lalu";
const { container } = renderLogin();
- expect(screen.getByRole("heading", { name: "Fami" })).toBeInTheDocument();
+ expect(screen.getByRole("heading", { name: "External Admin" })).toBeInTheDocument();
expect(container.querySelector("img")).toBeNull();
+ expect(document.title).toBe("External Admin");
+ expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument();
expect(screen.queryByRole("combobox", { name: "App" })).not.toBeInTheDocument();
await user.type(screen.getByRole("textbox", { name: "External admin account" }), "fami-manager");
+ expect(screen.getByRole("heading", { name: "External Admin" })).toBeInTheDocument();
+ expect(container.querySelector("img")).toBeNull();
await user.type(container.querySelector('input[autocomplete="current-password"]'), "secret-password");
await user.click(screen.getByRole("button", { name: "Sign in" }));
diff --git a/external-admin/src/shared/useExternalSessionBrand.js b/external-admin/src/shared/useExternalSessionBrand.js
new file mode 100644
index 0000000..f7cbbef
--- /dev/null
+++ b/external-admin/src/shared/useExternalSessionBrand.js
@@ -0,0 +1,32 @@
+import { useEffect } from "react";
+
+export function useExternalSessionBrand(session, fallbackTitle) {
+ const appName = String(session?.appName || session?.appCode || fallbackTitle || "External Admin").trim();
+ const logoUrl = String(session?.logoUrl || "").trim();
+
+ useEffect(() => {
+ // 认证后的 App 品牌只采用服务端 session;session 又来自 apps 行,前端不解析账号名称猜租户。
+ globalThis.document.title = appName;
+ syncBrandFavicon(logoUrl);
+ return () => {
+ globalThis.document.title = fallbackTitle || "External Admin";
+ syncBrandFavicon("");
+ };
+ }, [appName, fallbackTitle, logoUrl]);
+}
+
+function syncBrandFavicon(logoUrl) {
+ const selector = 'link[data-external-app-favicon="true"]';
+ const current = globalThis.document?.head?.querySelector(selector);
+ if (!logoUrl) {
+ current?.remove();
+ return;
+ }
+ const link = current || globalThis.document.createElement("link");
+ link.dataset.externalAppFavicon = "true";
+ link.rel = "icon";
+ link.href = logoUrl;
+ if (!current) {
+ globalThis.document.head.append(link);
+ }
+}
diff --git a/external-admin/src/styles/index.css b/external-admin/src/styles/index.css
index d0ed2ac..3bff03d 100644
--- a/external-admin/src/styles/index.css
+++ b/external-admin/src/styles/index.css
@@ -35,9 +35,9 @@ body {
min-width: 140px !important;
}
-.external-login-avatar {
- background: var(--brand-gradient) !important;
- color: #fff;
+.external-change-password-brand {
+ font-size: 18px;
+ font-weight: 750;
}
.external-full-state {
diff --git a/src/shared/ui/AppIdentity.test.jsx b/src/shared/ui/AppIdentity.test.jsx
index a4d3c88..ac57526 100644
--- a/src/shared/ui/AppIdentity.test.jsx
+++ b/src/shared/ui/AppIdentity.test.jsx
@@ -39,3 +39,17 @@ test("can hide the fallback when a surface must only show a database logo", () =
expect(screen.getByText("Fami")).toBeInTheDocument();
expect(screen.queryByText("F", { selector: "span" })).not.toBeInTheDocument();
});
+
+test("does not invent a fallback when an external portal database logo fails to load", () => {
+ const { container } = render(
+ ,
+ );
+
+ fireEvent.error(container.querySelector("img"));
+ expect(container.querySelector("img")).toBeNull();
+ expect(screen.getByText("Fami")).toBeInTheDocument();
+ expect(screen.queryByText("F", { selector: "span" })).not.toBeInTheDocument();
+});