fix(user): 普通用户接口不返回备注

- 用户侧 dto.User 移除 notes 字段,避免泄露管理员备注\n- 新增 dto.AdminUser 并调整 /admin/users 系列接口使用\n- 前端拆分 User/AdminUser,管理端用户页面使用 AdminUser\n- 更新契约测试:/api/v1/auth/me 响应不包含 notes
This commit is contained in:
墨颜
2026-01-19 19:23:51 +08:00
parent 4f4c9679bf
commit 00d9fbd220
12 changed files with 73 additions and 55 deletions

View File

@@ -84,9 +84,9 @@ func (h *UserHandler) List(c *gin.Context) {
return return
} }
out := make([]dto.User, 0, len(users)) out := make([]dto.AdminUser, 0, len(users))
for i := range users { for i := range users {
out = append(out, *dto.UserFromService(&users[i])) out = append(out, *dto.UserFromServiceAdmin(&users[i]))
} }
response.Paginated(c, out, total, page, pageSize) response.Paginated(c, out, total, page, pageSize)
} }
@@ -129,7 +129,7 @@ func (h *UserHandler) GetByID(c *gin.Context) {
return return
} }
response.Success(c, dto.UserFromService(user)) response.Success(c, dto.UserFromServiceAdmin(user))
} }
// Create handles creating a new user // Create handles creating a new user
@@ -155,7 +155,7 @@ func (h *UserHandler) Create(c *gin.Context) {
return return
} }
response.Success(c, dto.UserFromService(user)) response.Success(c, dto.UserFromServiceAdmin(user))
} }
// Update handles updating a user // Update handles updating a user
@@ -189,7 +189,7 @@ func (h *UserHandler) Update(c *gin.Context) {
return return
} }
response.Success(c, dto.UserFromService(user)) response.Success(c, dto.UserFromServiceAdmin(user))
} }
// Delete handles deleting a user // Delete handles deleting a user
@@ -231,7 +231,7 @@ func (h *UserHandler) UpdateBalance(c *gin.Context) {
return return
} }
response.Success(c, dto.UserFromService(user)) response.Success(c, dto.UserFromServiceAdmin(user))
} }
// GetUserAPIKeys handles getting user's API keys // GetUserAPIKeys handles getting user's API keys

View File

@@ -15,7 +15,6 @@ func UserFromServiceShallow(u *service.User) *User {
ID: u.ID, ID: u.ID,
Email: u.Email, Email: u.Email,
Username: u.Username, Username: u.Username,
Notes: u.Notes,
Role: u.Role, Role: u.Role,
Balance: u.Balance, Balance: u.Balance,
Concurrency: u.Concurrency, Concurrency: u.Concurrency,
@@ -48,6 +47,22 @@ func UserFromService(u *service.User) *User {
return out return out
} }
// UserFromServiceAdmin converts a service User to DTO for admin users.
// It includes notes - user-facing endpoints must not use this.
func UserFromServiceAdmin(u *service.User) *AdminUser {
if u == nil {
return nil
}
base := UserFromService(u)
if base == nil {
return nil
}
return &AdminUser{
User: *base,
Notes: u.Notes,
}
}
func APIKeyFromService(k *service.APIKey) *APIKey { func APIKeyFromService(k *service.APIKey) *APIKey {
if k == nil { if k == nil {
return nil return nil

View File

@@ -6,7 +6,6 @@ type User struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Email string `json:"email"` Email string `json:"email"`
Username string `json:"username"` Username string `json:"username"`
Notes string `json:"notes"`
Role string `json:"role"` Role string `json:"role"`
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
Concurrency int `json:"concurrency"` Concurrency int `json:"concurrency"`
@@ -19,6 +18,14 @@ type User struct {
Subscriptions []UserSubscription `json:"subscriptions,omitempty"` Subscriptions []UserSubscription `json:"subscriptions,omitempty"`
} }
// AdminUser 是管理员接口使用的 user DTO包含敏感/内部字段)。
// 注意:普通用户接口不得返回 notes 等管理员备注信息。
type AdminUser struct {
User
Notes string `json:"notes"`
}
type APIKey struct { type APIKey struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`

View File

@@ -47,9 +47,6 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
return return
} }
// 清空notes字段普通用户不应看到备注
userData.Notes = ""
response.Success(c, dto.UserFromService(userData)) response.Success(c, dto.UserFromService(userData))
} }
@@ -105,8 +102,5 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
return return
} }
// 清空notes字段普通用户不应看到备注
updatedUser.Notes = ""
response.Success(c, dto.UserFromService(updatedUser)) response.Success(c, dto.UserFromService(updatedUser))
} }

View File

@@ -51,7 +51,6 @@ func TestAPIContracts(t *testing.T) {
"id": 1, "id": 1,
"email": "alice@example.com", "email": "alice@example.com",
"username": "alice", "username": "alice",
"notes": "hello",
"role": "user", "role": "user",
"balance": 12.5, "balance": 12.5,
"concurrency": 5, "concurrency": 5,

View File

@@ -4,7 +4,7 @@
*/ */
import { apiClient } from '../client' import { apiClient } from '../client'
import type { User, UpdateUserRequest, PaginatedResponse } from '@/types' import type { AdminUser, UpdateUserRequest, PaginatedResponse } from '@/types'
/** /**
* List all users with pagination * List all users with pagination
@@ -26,7 +26,7 @@ export async function list(
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
} }
): Promise<PaginatedResponse<User>> { ): Promise<PaginatedResponse<AdminUser>> {
// Build params with attribute filters in attr[id]=value format // Build params with attribute filters in attr[id]=value format
const params: Record<string, any> = { const params: Record<string, any> = {
page, page,
@@ -44,8 +44,7 @@ export async function list(
} }
} }
} }
const { data } = await apiClient.get<PaginatedResponse<AdminUser>>('/admin/users', {
const { data } = await apiClient.get<PaginatedResponse<User>>('/admin/users', {
params, params,
signal: options?.signal signal: options?.signal
}) })
@@ -57,8 +56,8 @@ export async function list(
* @param id - User ID * @param id - User ID
* @returns User details * @returns User details
*/ */
export async function getById(id: number): Promise<User> { export async function getById(id: number): Promise<AdminUser> {
const { data } = await apiClient.get<User>(`/admin/users/${id}`) const { data } = await apiClient.get<AdminUser>(`/admin/users/${id}`)
return data return data
} }
@@ -73,8 +72,8 @@ export async function create(userData: {
balance?: number balance?: number
concurrency?: number concurrency?: number
allowed_groups?: number[] | null allowed_groups?: number[] | null
}): Promise<User> { }): Promise<AdminUser> {
const { data } = await apiClient.post<User>('/admin/users', userData) const { data } = await apiClient.post<AdminUser>('/admin/users', userData)
return data return data
} }
@@ -84,8 +83,8 @@ export async function create(userData: {
* @param updates - Fields to update * @param updates - Fields to update
* @returns Updated user * @returns Updated user
*/ */
export async function update(id: number, updates: UpdateUserRequest): Promise<User> { export async function update(id: number, updates: UpdateUserRequest): Promise<AdminUser> {
const { data } = await apiClient.put<User>(`/admin/users/${id}`, updates) const { data } = await apiClient.put<AdminUser>(`/admin/users/${id}`, updates)
return data return data
} }
@@ -112,8 +111,8 @@ export async function updateBalance(
balance: number, balance: number,
operation: 'set' | 'add' | 'subtract' = 'set', operation: 'set' | 'add' | 'subtract' = 'set',
notes?: string notes?: string
): Promise<User> { ): Promise<AdminUser> {
const { data } = await apiClient.post<User>(`/admin/users/${id}/balance`, { const { data } = await apiClient.post<AdminUser>(`/admin/users/${id}/balance`, {
balance, balance,
operation, operation,
notes: notes || '' notes: notes || ''
@@ -127,7 +126,7 @@ export async function updateBalance(
* @param concurrency - New concurrency limit * @param concurrency - New concurrency limit
* @returns Updated user * @returns Updated user
*/ */
export async function updateConcurrency(id: number, concurrency: number): Promise<User> { export async function updateConcurrency(id: number, concurrency: number): Promise<AdminUser> {
return update(id, { concurrency }) return update(id, { concurrency })
} }
@@ -137,7 +136,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis
* @param status - New status * @param status - New status
* @returns Updated user * @returns Updated user
*/ */
export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<User> { export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<AdminUser> {
return update(id, { status }) return update(id, { status })
} }

View File

@@ -39,10 +39,10 @@ import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { User, Group } from '@/types' import type { AdminUser, Group } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
const props = defineProps<{ show: boolean, user: User | null }>() const props = defineProps<{ show: boolean, user: AdminUser | null }>()
const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore() const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore()
const groups = ref<Group[]>([]); const selectedIds = ref<number[]>([]); const loading = ref(false); const submitting = ref(false) const groups = ref<Group[]>([]); const selectedIds = ref<number[]>([]); const loading = ref(false); const submitting = ref(false)

View File

@@ -32,10 +32,10 @@ import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import { formatDateTime } from '@/utils/format' import { formatDateTime } from '@/utils/format'
import type { User, ApiKey } from '@/types' import type { AdminUser, ApiKey } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
const props = defineProps<{ show: boolean, user: User | null }>() const props = defineProps<{ show: boolean, user: AdminUser | null }>()
defineEmits(['close']); const { t } = useI18n() defineEmits(['close']); const { t } = useI18n()
const apiKeys = ref<ApiKey[]>([]); const loading = ref(false) const apiKeys = ref<ApiKey[]>([]); const loading = ref(false)

View File

@@ -29,10 +29,10 @@ import { reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { User } from '@/types' import type { AdminUser } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
const props = defineProps<{ show: boolean, user: User | null, operation: 'add' | 'subtract' }>() const props = defineProps<{ show: boolean, user: AdminUser | null, operation: 'add' | 'subtract' }>()
const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore() const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore()
const submitting = ref(false); const form = reactive({ amount: 0, notes: '' }) const submitting = ref(false); const form = reactive({ amount: 0, notes: '' })

View File

@@ -56,12 +56,12 @@ import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { User, UserAttributeValuesMap } from '@/types' import type { AdminUser, UserAttributeValuesMap } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import UserAttributeForm from '@/components/user/UserAttributeForm.vue' import UserAttributeForm from '@/components/user/UserAttributeForm.vue'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
const props = defineProps<{ show: boolean, user: User | null }>() const props = defineProps<{ show: boolean, user: AdminUser | null }>()
const emit = defineEmits(['close', 'success']) const emit = defineEmits(['close', 'success'])
const { t } = useI18n(); const appStore = useAppStore(); const { copyToClipboard } = useClipboard() const { t } = useI18n(); const appStore = useAppStore(); const { copyToClipboard } = useClipboard()

View File

@@ -27,7 +27,6 @@ export interface FetchOptions {
export interface User { export interface User {
id: number id: number
username: string username: string
notes: string
email: string email: string
role: 'admin' | 'user' // User role for authorization role: 'admin' | 'user' // User role for authorization
balance: number // User balance for API usage balance: number // User balance for API usage
@@ -39,6 +38,11 @@ export interface User {
updated_at: string updated_at: string
} }
export interface AdminUser extends User {
// 管理员备注(普通用户接口不返回)
notes: string
}
export interface LoginRequest { export interface LoginRequest {
email: string email: string
password: string password: string

View File

@@ -492,7 +492,7 @@ import Icon from '@/components/icons/Icon.vue'
const { t } = useI18n() const { t } = useI18n()
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { User, UserAttributeDefinition } from '@/types' import type { AdminUser, UserAttributeDefinition } from '@/types'
import type { BatchUserUsageStats } from '@/api/admin/dashboard' import type { BatchUserUsageStats } from '@/api/admin/dashboard'
import type { Column } from '@/components/common/types' import type { Column } from '@/components/common/types'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
@@ -637,7 +637,7 @@ const columns = computed<Column[]>(() =>
) )
) )
const users = ref<User[]>([]) const users = ref<AdminUser[]>([])
const loading = ref(false) const loading = ref(false)
const searchQuery = ref('') const searchQuery = ref('')
@@ -736,16 +736,16 @@ const showEditModal = ref(false)
const showDeleteDialog = ref(false) const showDeleteDialog = ref(false)
const showApiKeysModal = ref(false) const showApiKeysModal = ref(false)
const showAttributesModal = ref(false) const showAttributesModal = ref(false)
const editingUser = ref<User | null>(null) const editingUser = ref<AdminUser | null>(null)
const deletingUser = ref<User | null>(null) const deletingUser = ref<AdminUser | null>(null)
const viewingUser = ref<User | null>(null) const viewingUser = ref<AdminUser | null>(null)
let abortController: AbortController | null = null let abortController: AbortController | null = null
// Action Menu State // Action Menu State
const activeMenuId = ref<number | null>(null) const activeMenuId = ref<number | null>(null)
const menuPosition = ref<{ top: number; left: number } | null>(null) const menuPosition = ref<{ top: number; left: number } | null>(null)
const openActionMenu = (user: User, e: MouseEvent) => { const openActionMenu = (user: AdminUser, e: MouseEvent) => {
if (activeMenuId.value === user.id) { if (activeMenuId.value === user.id) {
closeActionMenu() closeActionMenu()
} else { } else {
@@ -821,11 +821,11 @@ const handleClickOutside = (event: MouseEvent) => {
// Allowed groups modal state // Allowed groups modal state
const showAllowedGroupsModal = ref(false) const showAllowedGroupsModal = ref(false)
const allowedGroupsUser = ref<User | null>(null) const allowedGroupsUser = ref<AdminUser | null>(null)
// Balance (Deposit/Withdraw) modal state // Balance (Deposit/Withdraw) modal state
const showBalanceModal = ref(false) const showBalanceModal = ref(false)
const balanceUser = ref<User | null>(null) const balanceUser = ref<AdminUser | null>(null)
const balanceOperation = ref<'add' | 'subtract'>('add') const balanceOperation = ref<'add' | 'subtract'>('add')
// 计算剩余天数 // 计算剩余天数
@@ -998,7 +998,7 @@ const applyFilter = () => {
loadUsers() loadUsers()
} }
const handleEdit = (user: User) => { const handleEdit = (user: AdminUser) => {
editingUser.value = user editingUser.value = user
showEditModal.value = true showEditModal.value = true
} }
@@ -1008,7 +1008,7 @@ const closeEditModal = () => {
editingUser.value = null editingUser.value = null
} }
const handleToggleStatus = async (user: User) => { const handleToggleStatus = async (user: AdminUser) => {
const newStatus = user.status === 'active' ? 'disabled' : 'active' const newStatus = user.status === 'active' ? 'disabled' : 'active'
try { try {
await adminAPI.users.toggleStatus(user.id, newStatus) await adminAPI.users.toggleStatus(user.id, newStatus)
@@ -1022,7 +1022,7 @@ const handleToggleStatus = async (user: User) => {
} }
} }
const handleViewApiKeys = (user: User) => { const handleViewApiKeys = (user: AdminUser) => {
viewingUser.value = user viewingUser.value = user
showApiKeysModal.value = true showApiKeysModal.value = true
} }
@@ -1032,7 +1032,7 @@ const closeApiKeysModal = () => {
viewingUser.value = null viewingUser.value = null
} }
const handleAllowedGroups = (user: User) => { const handleAllowedGroups = (user: AdminUser) => {
allowedGroupsUser.value = user allowedGroupsUser.value = user
showAllowedGroupsModal.value = true showAllowedGroupsModal.value = true
} }
@@ -1042,7 +1042,7 @@ const closeAllowedGroupsModal = () => {
allowedGroupsUser.value = null allowedGroupsUser.value = null
} }
const handleDelete = (user: User) => { const handleDelete = (user: AdminUser) => {
deletingUser.value = user deletingUser.value = user
showDeleteDialog.value = true showDeleteDialog.value = true
} }
@@ -1061,13 +1061,13 @@ const confirmDelete = async () => {
} }
} }
const handleDeposit = (user: User) => { const handleDeposit = (user: AdminUser) => {
balanceUser.value = user balanceUser.value = user
balanceOperation.value = 'add' balanceOperation.value = 'add'
showBalanceModal.value = true showBalanceModal.value = true
} }
const handleWithdraw = (user: User) => { const handleWithdraw = (user: AdminUser) => {
balanceUser.value = user balanceUser.value = user
balanceOperation.value = 'subtract' balanceOperation.value = 'subtract'
showBalanceModal.value = true showBalanceModal.value = true