feat(auth): rich workspace-aware notification on successful login

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.
This commit is contained in:
wizardchen
2026-05-18 20:21:46 +08:00
committed by lyingbug
parent 0cdb922d84
commit 60a579d869
7 changed files with 67 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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',

View File

@@ -1147,6 +1147,9 @@ export default {
rememberMe: "로그인 상태 유지",
forgotPassword: "비밀번호를 잊으셨나요?",
loginSuccess: "로그인 성공!",
loginSuccessTitle: "로그인 성공",
loginSuccessContent: "환영합니다. 「{name}」에 입장했습니다",
loginSuccessContentWithRole: "환영합니다. 「{name}」에 입장했습니다 · 역할: {role}",
loginFailed: "로그인 실패",
loggingIn: "로그인 중...",
oidcLogin: "OIDC로 로그인",

View File

@@ -1248,6 +1248,9 @@ export default {
rememberMe: 'Запомнить меня',
forgotPassword: 'Забыли пароль?',
loginSuccess: 'Вход выполнен успешно!',
loginSuccessTitle: 'Вход выполнен',
loginSuccessContent: 'С возвращением. Вы вошли в «{name}»',
loginSuccessContentWithRole: 'С возвращением. Вы вошли в «{name}» · роль: {role}',
loginFailed: 'Ошибка входа',
loggingIn: 'Вход...',
register: 'Регистрация',

View File

@@ -1142,6 +1142,9 @@ export default {
rememberMe: "记住我",
forgotPassword: "忘记密码?",
loginSuccess: "登录成功!",
loginSuccessTitle: "登录成功",
loginSuccessContent: "欢迎,你已进入「{name}」",
loginSuccessContentWithRole: "欢迎,你已进入「{name}」,身份:{role}",
loginFailed: "登录失败",
loggingIn: "登录中...",
oidcLogin: "使用 OIDC 登录",

View File

@@ -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, unknown>) => 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,
})
}

View File

@@ -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'))
}