feat(ui): Add tenant info

This commit is contained in:
wizardchen
2025-09-16 15:39:34 +08:00
committed by lyingbug
parent d28f805707
commit 4137a63852
8 changed files with 637 additions and 54 deletions

View File

@@ -8,7 +8,6 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; frame-ancestors 'self';" always;
# 错误日志配置
error_log /var/log/nginx/error.log warn;

View File

@@ -74,8 +74,13 @@ export interface UserInfo {
export interface TenantInfo {
id: string
name: string
description?: string
api_key: string
status?: string
business?: string
owner_id: string
storage_quota?: number
storage_used?: number
created_at: string
updated_at: string
knowledge_bases?: KnowledgeBaseInfo[]

View File

@@ -3,50 +3,71 @@
<div class="logo_box">
<img class="logo" src="@/assets/img/weknora.png" alt="">
</div>
<div class="menu_box" v-for="(item, index) in menuArr" :key="index">
<div @click="gotopage(item.path)"
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
<div class="menu_item-box">
<div class="menu_icon">
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : prefixIcon)" alt="">
<!-- 上半部分知识库和对话 -->
<div class="menu_top">
<div class="menu_box" :class="{ 'has-submenu': item.children }" v-for="(item, index) in topMenuItems" :key="index">
<div @click="gotopage(item.path)"
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
<div class="menu_item-box">
<div class="menu_icon">
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
</div>
<span class="menu_title">{{ item.title }}</span>
</div>
<span class="menu_title">{{ item.title }}</span>
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
placement="top" show-arrow destroy-on-close>
<div class="upload-file-wrap" @click="uploadFile" variant="outline"
v-if="item.path == 'knowledgeBase'">
<img class="upload-file-icon" :class="[item.path == currentpath ? 'active-upload' : '']"
:src="getImgSrc(fileAddIcon)" alt="">
</div>
</t-popup>
</div>
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
placement="top" show-arrow destroy-on-close>
<div class="upload-file-wrap" @click="uploadFile" variant="outline"
v-if="item.path == 'knowledgeBase'">
<img class="upload-file-icon" :class="[item.path == currentpath ? 'active-upload' : '']"
:src="getImgSrc(fileAddIcon)" alt="">
</div>
</t-popup>
</div>
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
<div class="submenu_item_p" v-for="(subitem, subindex) in item.children" :key="subindex"
@click="gotopage(subitem.path)">
<div :class="['submenu_item', currentSecondpath == subitem.path ? 'submenu_item_active' : '']"
@mouseenter="mouseenteBotDownr(subindex)" @mouseleave="mouseleaveBotDown">
<i v-if="currentSecondpath == subitem.path" class="dot"></i>
<span class="submenu_title"
:style="currentSecondpath == subitem.path ? 'margin-left:14px;max-width:160px;' : 'margin-left:18px;max-width:173px;'">
{{ subitem.title }}
</span>
<t-popup v-model:visible="subitem.isMore" @overlay-click="delCard(subindex, subitem)"
@visible-change="onVisibleChange" overlayClassName="del-menu-popup" trigger="click"
destroy-on-close placement="top-left">
<div v-if="(activeSubmenu == subindex) || (currentSecondpath == subitem.path) || subitem.isMore"
@click.stop="openMore(subindex)" variant="outline" class="menu-more-wrap">
<t-icon name="ellipsis" class="menu-more" />
</div>
<template #content>
<span class="del_submenu">删除记录</span>
</template>
</t-popup>
<div ref="submenuscrollContainer" @scroll="handleScroll" class="submenu" v-if="item.children">
<div class="submenu_item_p" v-for="(subitem, subindex) in item.children" :key="subindex"
@click="gotopage(subitem.path)">
<div :class="['submenu_item', currentSecondpath == subitem.path ? 'submenu_item_active' : '']"
@mouseenter="mouseenteBotDownr(subindex)" @mouseleave="mouseleaveBotDown">
<i v-if="currentSecondpath == subitem.path" class="dot"></i>
<span class="submenu_title"
:style="currentSecondpath == subitem.path ? 'margin-left:14px;max-width:160px;' : 'margin-left:18px;max-width:173px;'">
{{ subitem.title }}
</span>
<t-popup v-model:visible="subitem.isMore" @overlay-click="delCard(subindex, subitem)"
@visible-change="onVisibleChange" overlayClassName="del-menu-popup" trigger="click"
destroy-on-close placement="top-left">
<div v-if="(activeSubmenu == subindex) || (currentSecondpath == subitem.path) || subitem.isMore"
@click.stop="openMore(subindex)" variant="outline" class="menu-more-wrap">
<t-icon name="ellipsis" class="menu-more" />
</div>
<template #content>
<span class="del_submenu">删除记录</span>
</template>
</t-popup>
</div>
</div>
</div>
</div>
</div>
<!-- 下半部分账户信息系统设置退出登录 -->
<div class="menu_bottom">
<div class="menu_box" v-for="(item, index) in bottomMenuItems" :key="'bottom-' + index">
<div @click="gotopage(item.path)"
@mouseenter="mouseenteMenu(item.path)" @mouseleave="mouseleaveMenu(item.path)"
:class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']">
<div class="menu_item-box">
<div class="menu_icon">
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'setting' ? settingIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
</div>
<span class="menu_title">{{ item.title }}</span>
</div>
</div>
</div>
</div>
<input type="file" @change="upload" style="display: none" ref="uploadInput"
accept=".pdf,.docx,.doc,.txt,.md,.jpg,.jpeg,.png" />
</div>
@@ -78,6 +99,19 @@ const totalPages = computed(() => Math.ceil(total.value / page_size.value));
const hasMore = computed(() => currentPage.value < totalPages.value);
const { menuArr } = storeToRefs(usemenuStore);
let activeSubmenu = ref(-1);
// 分离上下两部分菜单
const topMenuItems = computed(() => {
return menuArr.value.filter(item =>
item.path === 'knowledgeBase' || item.path === 'creatChat'
);
});
const bottomMenuItems = computed(() => {
return menuArr.value.filter(item =>
item.path !== 'knowledgeBase' && item.path !== 'creatChat'
);
});
const loading = ref(false)
const uploadFile = () => {
uploadInput.value.click()
@@ -167,12 +201,14 @@ let knowledgeIcon = ref('zhishiku-green.svg');
let prefixIcon = ref('prefixIcon.svg');
let settingIcon = ref('setting.svg');
let logoutIcon = ref('logout.svg');
let tenantIcon = ref('setting.svg'); // 暂时使用setting图标
let pathPrefix = ref(route.name)
const getIcon = (path) => {
fileAddIcon.value = path == 'knowledgeBase' ? 'file-add-green.svg' : 'file-add.svg';
knowledgeIcon.value = path == 'knowledgeBase' ? 'zhishiku-green.svg' : 'zhishiku.svg';
prefixIcon.value = path == 'creatChat' ? 'prefixIcon-green.svg' : path == 'knowledgeBase' ? 'prefixIcon-grey.svg' : 'prefixIcon.svg';
settingIcon.value = path == 'settings' ? 'setting-green.svg' : 'setting.svg';
tenantIcon.value = path == 'tenant' ? 'setting-green.svg' : 'setting.svg'; // 暂时使用setting图标
logoutIcon.value = 'logout.svg';
}
getIcon(route.name)
@@ -221,6 +257,10 @@ const mouseleaveMenu = (path) => {
padding: 8px;
background: #fff;
box-sizing: border-box;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
.logo_box {
height: 80px;
@@ -250,9 +290,28 @@ const mouseleaveMenu = (path) => {
line-height: 21.7px;
}
.menu_top {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.menu_bottom {
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.menu_box {
display: flex;
flex-direction: column;
&.has-submenu {
flex: 1;
min-height: 0;
}
}
@@ -358,12 +417,11 @@ const mouseleaveMenu = (path) => {
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
overflow-y: scroll;
overflow-y: auto;
scrollbar-width: none;
height: calc(98vh - 276px);
flex: 1;
min-height: 0;
margin-left: 4px;
}
.submenu_item_p {

View File

@@ -35,6 +35,12 @@ const router = createRouter({
component: () => import("../views/platform/index.vue"),
meta: { requiresInit: true, requiresAuth: true },
children: [
{
path: "tenant",
name: "tenant",
component: () => import("../views/tenant/TenantInfo.vue"),
meta: { requiresInit: true, requiresAuth: true }
},
{
path: "knowledgeBase",
name: "knowledgeBase",

View File

@@ -13,6 +13,7 @@ export const useMenuStore = defineStore('menuStore', {
childrenPath: 'chat',
children: reactive<object[]>([]),
},
{ title: '账户信息', icon: 'tenant', path: 'tenant' },
{ title: '系统设置', icon: 'setting', path: 'settings' },
{ title: '退出登录', icon: 'logout', path: 'logout' }
]),
@@ -31,7 +32,7 @@ export const useMenuStore = defineStore('menuStore', {
this.menuArr[1].children?.unshift(item)
},
updatasessionTitle(session_id: string, title: string) {
this.menuArr[1].children?.forEach(item => {
this.menuArr[1].children?.forEach((item: any) => {
if (item.id == session_id) {
item.title = title;
item.isNoTitle = false;

View File

@@ -0,0 +1,496 @@
<template>
<div class="tenant-info-container">
<div class="tenant-header">
<h2>账户信息</h2>
<p class="tenant-subtitle">查看和管理您的用户账户和租户配置信息</p>
</div>
<div class="tenant-content" v-if="!loading && !error">
<!-- 用户信息卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-title">用户信息</div>
</template>
<div class="info-content">
<t-descriptions :column="1" layout="vertical">
<t-descriptions-item label="用户 ID">
{{ userInfo?.id }}
</t-descriptions-item>
<t-descriptions-item label="用户名">
{{ userInfo?.username }}
</t-descriptions-item>
<t-descriptions-item label="邮箱">
{{ userInfo?.email }}
</t-descriptions-item>
<t-descriptions-item label="创建时间">
{{ formatDate(userInfo?.created_at) }}
</t-descriptions-item>
</t-descriptions>
</div>
</t-card>
<!-- 租户信息卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-title">租户信息</div>
</template>
<div class="info-content">
<t-descriptions :column="1" layout="vertical">
<t-descriptions-item label="租户 ID">
{{ tenantInfo?.id }}
</t-descriptions-item>
<t-descriptions-item label="租户名称">
{{ tenantInfo?.name }}
</t-descriptions-item>
<t-descriptions-item label="描述">
{{ tenantInfo?.description || '暂无描述' }}
</t-descriptions-item>
<t-descriptions-item label="业务">
{{ tenantInfo?.business || '暂无' }}
</t-descriptions-item>
<t-descriptions-item label="状态">
<t-tag
:theme="getStatusTheme(tenantInfo?.status)"
variant="light"
>
{{ getStatusText(tenantInfo?.status) }}
</t-tag>
</t-descriptions-item>
<t-descriptions-item label="创建时间">
{{ formatDate(tenantInfo?.created_at) }}
</t-descriptions-item>
</t-descriptions>
</div>
</t-card>
<!-- API Key 卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-header-with-actions">
<div class="card-title">API Key</div>
</div>
</template>
<div class="api-key-content">
<t-input
v-model="displayApiKey"
readonly
class="api-key-input"
:type="showApiKey ? 'text' : 'password'"
/>
<t-alert theme="warning" :close="false" class="api-warning">
<template #icon>
<t-icon name="error-circle" />
</template>
请妥善保管您的 API Key不要在公共场所或代码仓库中暴露
</t-alert>
</div>
</t-card>
<!-- 存储信息卡片 -->
<t-card
class="info-card"
:bordered="false"
v-if="tenantInfo?.storage_quota !== undefined"
>
<template #header>
<div class="card-title">存储信息</div>
</template>
<div class="storage-content">
<t-descriptions :column="1" layout="vertical">
<t-descriptions-item label="存储配额">
{{ formatBytes(tenantInfo.storage_quota) }}
</t-descriptions-item>
<t-descriptions-item label="已使用">
{{ formatBytes(tenantInfo.storage_used || 0) }}
</t-descriptions-item>
<t-descriptions-item label="使用率">
<div class="usage-info">
<span class="usage-text">{{ getUsagePercentage() }}%</span>
<t-progress
:percentage="getUsagePercentage()"
:show-info="false"
size="medium"
:theme="getUsagePercentage() > 80 ? 'warning' : 'success'"
/>
</div>
</t-descriptions-item>
</t-descriptions>
</div>
</t-card>
<!-- API 开发文档卡片 -->
<t-card class="info-card" :bordered="false">
<template #header>
<div class="card-title">API 开发文档</div>
</template>
<div class="doc-content">
<p class="doc-description">使用您的 API Key 开始开发查看完整的 API 文档和示例代码</p>
<t-space class="doc-actions">
<t-button
theme="primary"
@click="openApiDoc"
>
<template #icon>
<t-icon name="link" />
</template>
查看 API 文档
</t-button>
</t-space>
</div>
</t-card>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<t-loading size="large" />
<p class="loading-text">正在加载账户信息...</p>
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-container">
<t-result theme="error" title="加载失败" :description="error">
<template #extra>
<t-button theme="primary" @click="loadTenantInfo">重试</t-button>
</template>
</t-result>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { getCurrentUser, type TenantInfo, type UserInfo } from '@/api/auth'
// 响应式数据
const tenantInfo = ref<TenantInfo | null>(null)
const userInfo = ref<UserInfo | null>(null)
const loading = ref(true)
const error = ref('')
const showApiKey = ref(false)
const showApiExample = ref(false)
// API 基础 URL
const apiBaseUrl = window.location.origin
// 计算属性
const displayApiKey = computed(() => {
if (!tenantInfo.value?.api_key) return ''
return tenantInfo.value.api_key
})
// API示例代码
const apiExampleCode = computed(() => {
return `curl -X GET "${apiBaseUrl}/api/v1/tenants/${tenantInfo.value?.id}" \\
-H "Content-Type: application/json" \\
-H "X-API-Key: ${tenantInfo.value?.api_key}"`
})
// 方法
const loadTenantInfo = async () => {
try {
loading.value = true
error.value = ''
const response = await getCurrentUser()
if (response.success && response.data) {
userInfo.value = response.data.user
tenantInfo.value = response.data.tenant
} else {
error.value = response.message || '获取用户信息失败'
}
} catch (err: any) {
error.value = err.message || '网络错误,请稍后重试'
} finally {
loading.value = false
}
}
const toggleApiKeyVisibility = () => {
showApiKey.value = !showApiKey.value
}
const copyApiKey = async () => {
if (!tenantInfo.value?.api_key) return
try {
await navigator.clipboard.writeText(tenantInfo.value.api_key)
// 使用TDesign的消息组件
import('tdesign-vue-next').then(({ MessagePlugin }) => {
MessagePlugin.success('API Key 已复制到剪贴板')
})
} catch (err) {
// 降级到传统方式
const textArea = document.createElement('textarea')
textArea.value = tenantInfo.value.api_key
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
import('tdesign-vue-next').then(({ MessagePlugin }) => {
MessagePlugin.success('API Key 已复制到剪贴板')
})
}
}
const openApiDoc = () => {
window.open('https://github.com/Tencent/WeKnora/blob/main/docs/API.md', '_blank')
}
const getStatusText = (status: string | undefined) => {
switch (status) {
case 'active':
return '活跃'
case 'inactive':
return '未激活'
case 'suspended':
return '已暂停'
default:
return '未知'
}
}
const getStatusTheme = (status: string | undefined) => {
switch (status) {
case 'active':
return 'success'
case 'inactive':
return 'warning'
case 'suspended':
return 'danger'
default:
return 'default'
}
}
const formatDate = (dateStr: string | undefined) => {
if (!dateStr) return '未知'
try {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch {
return '格式错误'
}
}
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const getUsagePercentage = () => {
if (!tenantInfo.value?.storage_quota || tenantInfo.value.storage_quota === 0) {
return 0
}
const used = tenantInfo.value.storage_used || 0
const percentage = (used / tenantInfo.value.storage_quota) * 100
return Math.min(Math.round(percentage * 100) / 100, 100) // 保留两位小数最大100%
}
// 生命周期
onMounted(() => {
loadTenantInfo()
})
</script>
<style lang="less" scoped>
.tenant-info-container {
padding: 20px;
background-color: #fff;
border-radius: 8px;
margin: 0 20px 20px 20px;
height: calc(100vh - 40px);
overflow-y: auto;
box-sizing: border-box;
}
.tenant-header {
margin-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 16px;
h2 {
font-size: 20px;
font-weight: 600;
color: #000000;
margin: 0 0 8px 0;
}
.tenant-subtitle {
font-size: 14px;
color: #666666;
margin: 0;
}
}
.tenant-content {
display: grid;
gap: 20px;
grid-template-columns: 1fr;
}
.info-card {
margin-bottom: 20px;
.card-title {
font-size: 16px;
font-weight: 600;
color: #000000;
}
.card-header-with-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.info-content,
.api-key-content,
.storage-content,
.doc-content {
// padding: 16px 0;
}
.api-key-input {
margin-bottom: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.api-warning {
margin-top: 16px;
}
.usage-info {
display: flex;
flex-direction: column;
gap: 8px;
.usage-text {
font-weight: 500;
color: #000000;
}
}
.doc-description {
margin-bottom: 16px;
color: #666666;
font-size: 14px;
}
.doc-actions {
margin-bottom: 20px;
}
.api-example {
margin-top: 20px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 6px;
.example-header h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #000000;
}
.code-textarea {
margin-bottom: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.example-note {
margin-top: 16px;
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 24px;
text-align: center;
.loading-text {
margin-top: 16px;
color: #666666;
font-size: 14px;
}
}
.error-container {
padding: 40px;
text-align: center;
}
/* 响应式设计 */
@media (max-width: 768px) {
.tenant-info-container {
padding: 16px;
margin: 10px;
height: calc(100vh - 20px);
}
.tenant-header h2 {
font-size: 18px;
}
.card-header-with-actions {
flex-direction: column;
align-items: flex-start !important;
gap: 12px;
}
.doc-actions {
:deep(.t-space) {
flex-direction: column;
width: 100%;
.t-button {
width: 100%;
}
}
}
}
/* 覆盖TDesign组件样式 */
:deep(.t-card) {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
:deep(.t-descriptions-item__label) {
font-weight: 500;
color: #374151;
}
:deep(.t-descriptions-item__content) {
color: #000000;
}
:deep(.t-input__inner) {
font-family: inherit;
}
:deep(.code-textarea .t-textarea__inner) {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
}
</style>

View File

@@ -399,10 +399,10 @@ func (s *userService) RevokeToken(ctx context.Context, tokenString string) error
// GetCurrentUser gets current user from context
func (s *userService) GetCurrentUser(ctx context.Context) (*types.User, error) {
userID, ok := ctx.Value("user_id").(string)
user, ok := ctx.Value("user").(*types.User)
if !ok {
return nil, errors.New("user not found in context")
}
return s.userRepo.GetUserByID(ctx, userID)
return user, nil
}

View File

@@ -16,17 +16,20 @@ import (
// Provides functionality for user registration, login, logout, and token management
// through the REST API endpoints
type AuthHandler struct {
userService interfaces.UserService
userService interfaces.UserService
tenantService interfaces.TenantService
}
// NewAuthHandler creates a new auth handler instance with the provided service
// NewAuthHandler creates a new auth handler instance with the provided services
// Parameters:
// - userService: An implementation of the UserService interface for business logic
// - tenantService: An implementation of the TenantService interface for tenant management
//
// Returns a pointer to the newly created AuthHandler
func NewAuthHandler(userService interfaces.UserService) *AuthHandler {
func NewAuthHandler(userService interfaces.UserService, tenantService interfaces.TenantService) *AuthHandler {
return &AuthHandler{
userService: userService,
userService: userService,
tenantService: tenantService,
}
}
@@ -214,7 +217,7 @@ func (h *AuthHandler) RefreshToken(c *gin.Context) {
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Get current user info")
logger.Debugf(ctx, "Get current user info")
// Get current user from service (which extracts from context)
user, err := h.userService.GetCurrentUser(ctx)
@@ -225,10 +228,25 @@ func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
return
}
logger.Infof(ctx, "Retrieved current user info: %s", user.Email)
// Get tenant information
var tenant *types.Tenant
if user.TenantID > 0 {
tenant, err = h.tenantService.GetTenantByID(ctx, user.TenantID)
if err != nil {
logger.Warnf(ctx, "Failed to get tenant info for user %s, tenant ID %d: %v", user.Email, user.TenantID, err)
// Don't fail the request if tenant info is not available
} else {
logger.Debugf(ctx, "Retrieved tenant info for user %s: %s", user.Email, tenant.Name)
}
}
logger.Debugf(ctx, "Retrieved current user info: %s", user.Email)
c.JSON(http.StatusOK, gin.H{
"success": true,
"user": user.ToUserInfo(),
"data": gin.H{
"user": user.ToUserInfo(),
"tenant": tenant,
},
})
}