diff --git a/client/auth.go b/client/auth.go index 41a3884e9..9f8378c56 100644 --- a/client/auth.go +++ b/client/auth.go @@ -16,13 +16,27 @@ type LoginRequest struct { // LoginResponse is the body returned by POST /api/v1/auth/login. // // Token is the JWT access token; RefreshToken renews it via Auth.Refresh. +// ActiveTenant is the tenant whose ID is encoded in the JWT — every +// subsequent request is scoped to it until /auth/switch-tenant is called. +// Memberships lists every tenant the user can access along with their +// role in each, so callers can build a tenant switcher UI without a +// follow-up request. type LoginResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - User *AuthUser `json:"user,omitempty"` - Tenant *AuthTenant `json:"tenant,omitempty"` - Token string `json:"token,omitempty"` - RefreshToken string `json:"refresh_token,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + User *AuthUser `json:"user,omitempty"` + ActiveTenant *AuthTenant `json:"active_tenant,omitempty"` + Memberships []AuthMembership `json:"memberships,omitempty"` + Token string `json:"token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` +} + +// AuthMembership pairs a tenant ID with the user's role in that tenant. +// Mirrors types.Membership on the server. +type AuthMembership struct { + TenantID uint64 `json:"tenant_id"` + TenantName string `json:"tenant_name,omitempty"` + Role string `json:"role"` } // AuthUser is the principal returned by /auth/login and /auth/me. diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts index 7edbada07..2ec1ebcb4 100644 --- a/frontend/src/api/auth/index.ts +++ b/frontend/src/api/auth/index.ts @@ -175,6 +175,29 @@ export async function getOIDCConfig(): Promise { } } +/** + * 获取认证配置(仅返回前端渲染需要的公开字段,例如注册模式)。 + * + * 后端通过 `auth.registration_mode` 控制是否允许自助注册: + * - "self_serve" 保留现有自助注册入口(默认) + * - "invite_only" 关闭注册,要求管理员邀请 + * + * 失败时回落到 self_serve,避免接口异常导致注册入口直接消失。 + */ +export interface AuthConfigResponse { + success: boolean + registration_mode: 'self_serve' | 'invite_only' | string +} + +export async function getAuthConfig(): Promise { + try { + const response = await get('/api/v1/auth/config') + return response as unknown as AuthConfigResponse + } catch { + return { success: false, registration_mode: 'self_serve' } + } +} + /** * 用户注册 */ diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index 0e7dc23b3..4f0d161e5 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -27,6 +27,11 @@ export const useAuthStore = defineStore('auth', () => { const selectedTenantId = ref(null) const selectedTenantName = ref(null) const allTenants = ref([]) + // memberships lists every tenant the user can authenticate into, + // along with their role in each. Populated from /auth/login response. + // v1 deployments will typically have length 1; the field is wired now + // so PR 3 can render a tenant-switcher UI without a store migration. + const memberships = ref>([]) const isLiteMode = ref(false) // 计算属性 @@ -50,6 +55,17 @@ export const useAuthStore = defineStore('auth', () => { return user.value?.can_access_all_tenants || false }) + // currentTenantRole returns the user's role in the active tenant + // (defaulting to '' when memberships have not been loaded). Used by + // role-aware UI gating; PR 2 wires backend enforcement, PR 3 uses + // this for menu/button visibility. + const currentTenantRole = computed(() => { + const tid = tenant.value?.id ? String(tenant.value.id) : '' + if (!tid) return '' + const match = memberships.value.find((m) => String(m.tenant_id) === tid) + return match?.role || '' + }) + const effectiveTenantId = computed(() => { // 如果选择了其他租户,使用选择的租户ID,否则使用用户默认租户ID return selectedTenantId.value || (tenant.value?.id ? Number(tenant.value.id) : null) @@ -115,6 +131,13 @@ export const useAuthStore = defineStore('auth', () => { allTenants.value = tenants } + const setMemberships = ( + list: Array<{ tenant_id: number; tenant_name?: string; role: string }> + ) => { + memberships.value = Array.isArray(list) ? list : [] + localStorage.setItem('weknora_memberships', JSON.stringify(memberships.value)) + } + const getSelectedTenant = () => { return selectedTenantId.value } @@ -139,6 +162,7 @@ export const useAuthStore = defineStore('auth', () => { selectedTenantId.value = null selectedTenantName.value = null allTenants.value = [] + memberships.value = [] // 清空localStorage localStorage.removeItem('weknora_user') @@ -149,6 +173,7 @@ export const useAuthStore = defineStore('auth', () => { localStorage.removeItem('weknora_current_kb') localStorage.removeItem('weknora_selected_tenant_id') localStorage.removeItem('weknora_selected_tenant_name') + localStorage.removeItem('weknora_memberships') localStorage.removeItem('weknora_lite_mode') isLiteMode.value = false try { @@ -225,6 +250,17 @@ export const useAuthStore = defineStore('auth', () => { } } + const storedMemberships = localStorage.getItem('weknora_memberships') + if (storedMemberships) { + try { + const parsed = JSON.parse(storedMemberships) + memberships.value = Array.isArray(parsed) ? parsed : [] + } catch (e) { + console.error('Failed to parse memberships', e) + memberships.value = [] + } + } + isLiteMode.value = localStorage.getItem('weknora_lite_mode') === 'true' } @@ -242,16 +278,18 @@ export const useAuthStore = defineStore('auth', () => { selectedTenantId, selectedTenantName, allTenants, - + memberships, + // 计算属性 isLoggedIn, hasValidTenant, currentTenantId, currentUserId, canAccessAllTenants, + currentTenantRole, effectiveTenantId, isLiteMode, - + // 方法 setUser, setTenant, @@ -261,6 +299,7 @@ export const useAuthStore = defineStore('auth', () => { setCurrentKnowledgeBase, setSelectedTenant, setAllTenants, + setMemberships, getSelectedTenant, setLiteMode, logout, diff --git a/frontend/src/views/auth/Login.vue b/frontend/src/views/auth/Login.vue index 5674bca1e..ab404d243 100755 --- a/frontend/src/views/auth/Login.vue +++ b/frontend/src/views/auth/Login.vue @@ -229,7 +229,7 @@ {{ loading ? $t('auth.loggingIn') : $t('auth.login') }} -