From 60a579d8697c7ab396f4970e7d1f38dff96ea4bf Mon Sep 17 00:00:00 2001 From: wizardchen Date: Mon, 18 May 2026 20:21:46 +0800 Subject: [PATCH] feat(auth): rich workspace-aware notification on successful login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the plain "Login successful" toast with a TDesign Notify card that surfaces the workspace the user just landed in and their role there, so users immediately see which space they're working in — especially useful now that login can drop them into a remembered non-home tenant. A small shared helper (utils/loginNotify.ts) handles both the password login path (views/auth/Login.vue) and the OIDC callback path (App.vue handleGlobalOIDCCallback), so the two flows stay consistent. Role label goes through the same useRoleLabel composable used by the rest of the role-aware UI, so locale + future role-name tweaks live in one place. The membership lookup falls back gracefully (no role line) when the active tenant isn't in the response's memberships list — e.g. the auto-setup path that synthesises a single owner row. The legacy auth.loginSuccess key is left in place; nothing else references it, so a future cleanup PR can drop it safely. --- frontend/src/App.vue | 5 +++- frontend/src/i18n/locales/en-US.ts | 3 ++ frontend/src/i18n/locales/ko-KR.ts | 3 ++ frontend/src/i18n/locales/ru-RU.ts | 3 ++ frontend/src/i18n/locales/zh-CN.ts | 3 ++ frontend/src/utils/loginNotify.ts | 47 ++++++++++++++++++++++++++++++ frontend/src/views/auth/Login.vue | 5 +++- 7 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/loginNotify.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3d7f4910..414eabaa 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -8,6 +8,8 @@ import { useAuthStore } from '@/stores/auth' import { useSettingsStore } from '@/stores/settings' import { getCurrentUser } from '@/api/auth' import { consumePendingTenantSwitchToast } from '@/utils/tenantSwitch' +import { useRoleLabel } from '@/composables/useRoleLabel' +import { notifyLoginSuccess } from '@/utils/loginNotify' // TDesign locale configs import enUSConfig from 'tdesign-vue-next/esm/locale/en_US' @@ -16,6 +18,7 @@ import koKRConfig from 'tdesign-vue-next/esm/locale/ko_KR' import ruRUConfig from 'tdesign-vue-next/esm/locale/ru_RU' const { locale, t } = useI18n() +const { formatRole } = useRoleLabel() const router = useRouter() const authStore = useAuthStore() const settingsStore = useSettingsStore() @@ -139,8 +142,8 @@ const handleGlobalOIDCCallback = async () => { const response = decodeOIDCResult(oidcResult) if (response.success) { clearOIDCCallbackState('/') - MessagePlugin.success('Login successful') await persistOIDCLoginResponse(response) + notifyLoginSuccess(response, t, formatRole) return } diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 8dc121d2..f554153f 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -1296,6 +1296,9 @@ export default { rememberMe: 'Remember Me', forgotPassword: 'Forgot Password?', loginSuccess: 'Login successful!', + loginSuccessTitle: 'Login successful', + loginSuccessContent: 'Welcome back. You are now in "{name}"', + loginSuccessContentWithRole: 'Welcome back. You are now in "{name}" as {role}', loginFailed: 'Login failed', loggingIn: 'Logging in...', register: 'Register', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index fc5e362c..80e64057 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -1147,6 +1147,9 @@ export default { rememberMe: "로그인 상태 유지", forgotPassword: "비밀번호를 잊으셨나요?", loginSuccess: "로그인 성공!", + loginSuccessTitle: "로그인 성공", + loginSuccessContent: "환영합니다. 「{name}」에 입장했습니다", + loginSuccessContentWithRole: "환영합니다. 「{name}」에 입장했습니다 · 역할: {role}", loginFailed: "로그인 실패", loggingIn: "로그인 중...", oidcLogin: "OIDC로 로그인", diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index bb17a075..1a22672f 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -1248,6 +1248,9 @@ export default { rememberMe: 'Запомнить меня', forgotPassword: 'Забыли пароль?', loginSuccess: 'Вход выполнен успешно!', + loginSuccessTitle: 'Вход выполнен', + loginSuccessContent: 'С возвращением. Вы вошли в «{name}»', + loginSuccessContentWithRole: 'С возвращением. Вы вошли в «{name}» · роль: {role}', loginFailed: 'Ошибка входа', loggingIn: 'Вход...', register: 'Регистрация', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index e7f17091..182432f4 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1142,6 +1142,9 @@ export default { rememberMe: "记住我", forgotPassword: "忘记密码?", loginSuccess: "登录成功!", + loginSuccessTitle: "登录成功", + loginSuccessContent: "欢迎,你已进入「{name}」", + loginSuccessContentWithRole: "欢迎,你已进入「{name}」,身份:{role}", loginFailed: "登录失败", loggingIn: "登录中...", oidcLogin: "使用 OIDC 登录", diff --git a/frontend/src/utils/loginNotify.ts b/frontend/src/utils/loginNotify.ts new file mode 100644 index 00000000..c0b3e02f --- /dev/null +++ b/frontend/src/utils/loginNotify.ts @@ -0,0 +1,47 @@ +// Rich "you're now in {workspace} as {role}" notification for the +// post-login moment. Shared between the password (views/auth/Login.vue) +// and OIDC (App.vue handleGlobalOIDCCallback) login paths so the two +// flows feel identical to the user. +// +// Kept as a free function — not a composable — because the caller +// already has `t` and `formatRole` in scope from useI18n / useRoleLabel +// and there is no per-instance state worth tracking. + +import { NotifyPlugin } from 'tdesign-vue-next' + +type Translator = (key: string, params?: Record) => string +type RoleFormatter = (role: string | null | undefined) => string + +interface LoginResponseLike { + // Password-login response uses `active_tenant`; the OIDC callback + // response uses `tenant` (legacy backward-compat name on the Go side). + // Accept either so callers don't have to normalise. + active_tenant?: { id?: number | string; name?: string } + tenant?: { id?: number | string; name?: string } + memberships?: Array<{ tenant_id?: number | string; role?: string }> +} + +export function notifyLoginSuccess( + response: LoginResponseLike | null | undefined, + t: Translator, + formatRole: RoleFormatter, +): void { + const activeTenant = response?.active_tenant || response?.tenant + if (!activeTenant) return + + const tenantName = activeTenant.name || String(activeTenant.id || '') + const activeTenantId = Number(activeTenant.id) + const membership = Array.isArray(response?.memberships) + ? response!.memberships!.find((m) => Number(m?.tenant_id) === activeTenantId) + : null + const roleLabel = membership?.role ? formatRole(membership.role) : '' + + NotifyPlugin.success({ + title: t('auth.loginSuccessTitle'), + content: roleLabel + ? t('auth.loginSuccessContentWithRole', { name: tenantName, role: roleLabel }) + : t('auth.loginSuccessContent', { name: tenantName }), + duration: 6000, + closeBtn: true, + }) +} diff --git a/frontend/src/views/auth/Login.vue b/frontend/src/views/auth/Login.vue index d1455991..7ed9ced6 100755 --- a/frontend/src/views/auth/Login.vue +++ b/frontend/src/views/auth/Login.vue @@ -372,6 +372,8 @@ import { ref, reactive, nextTick, onMounted, onBeforeUnmount, computed } from 'vue' import { useRouter } from 'vue-router' import { MessagePlugin } from 'tdesign-vue-next' +import { useRoleLabel } from '@/composables/useRoleLabel' +import { notifyLoginSuccess } from '@/utils/loginNotify' import { Swiper, SwiperSlide } from 'swiper/vue' import { Autoplay, EffectFade, Pagination } from 'swiper/modules' import 'swiper/css' @@ -389,6 +391,7 @@ import screenshot4 from '@/assets/img/screenshot-4.svg' const router = useRouter() const authStore = useAuthStore() const { t, locale } = useI18n() +const { formatRole } = useRoleLabel() // Swiper modules const modules = [Autoplay, EffectFade, Pagination] @@ -662,8 +665,8 @@ const handleLogin = async () => { }) if (response.success) { - MessagePlugin.success(t('auth.loginSuccess')) await persistLoginResponse(response) + notifyLoginSuccess(response, t, formatRole) } else { MessagePlugin.error(response.message || t('auth.loginError')) }