diff --git a/backend/.env.example b/backend/.env.example index 99ac588..f5e92c6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -16,3 +16,6 @@ PORT=8080 ADMIN_USERNAME=admin ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=admin123 + +# API Token (用于外部 API 调用,可选) +API_TOKEN=your-api-token-here diff --git a/backend/internal/middleware/auth_middleware.go b/backend/internal/middleware/auth_middleware.go index 0ff76f1..5646b31 100644 --- a/backend/internal/middleware/auth_middleware.go +++ b/backend/internal/middleware/auth_middleware.go @@ -3,6 +3,7 @@ package middleware import ( "context" "net/http" + "os" "strings" "gpt-manager-go/internal/auth" @@ -31,7 +32,21 @@ func AuthMiddleware(next http.Handler) http.Handler { tokenString := parts[1] - // 解析 Token + // 首先检查是否是 API Token + apiToken := os.Getenv("API_TOKEN") + if apiToken != "" && tokenString == apiToken { + // API Token 认证成功,创建虚拟管理员上下文 + claims := &auth.Claims{ + UserID: 0, + Username: "api_token", + IsSuperAdmin: true, + } + ctx := context.WithValue(r.Context(), UserContextKey, claims) + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + + // 解析 JWT Token claims, err := auth.ParseToken(tokenString) if err != nil { http.Error(w, `{"success":false,"message":"Invalid or expired token"}`, http.StatusUnauthorized) diff --git a/frontend/src/api/invite.ts b/frontend/src/api/invite.ts index 546bf9b..0cd6f77 100644 --- a/frontend/src/api/invite.ts +++ b/frontend/src/api/invite.ts @@ -37,3 +37,20 @@ export function listInvitations(accountId: number) { export function deleteInvite(data: DeleteInviteRequest) { return request.delete('/api/invite', { data }) } + +export interface AdminInviteRequest { + email: string + account_id: number +} + +export interface AdminInviteResponse { + success: boolean + message?: string + invitation_id?: number + account_name?: string +} + +export function inviteByAdmin(data: AdminInviteRequest) { + return request.post('/api/invite', data) +} + diff --git a/frontend/src/stores/accounts.ts b/frontend/src/stores/accounts.ts index 5633a80..c6d36e6 100644 --- a/frontend/src/stores/accounts.ts +++ b/frontend/src/stores/accounts.ts @@ -19,8 +19,8 @@ export const useAccountsStore = defineStore('accounts', () => { error.value = null try { const response = await getAccounts() - if (response.data.success && response.data.data) { - accounts.value = response.data.data + if (response.data.success) { + accounts.value = response.data.data || [] } else { throw new Error(response.data.message || '获取账号列表失败') } diff --git a/frontend/src/views/admin/CardKeysPage.vue b/frontend/src/views/admin/CardKeysPage.vue index 32fc063..c8d9280 100644 --- a/frontend/src/views/admin/CardKeysPage.vue +++ b/frontend/src/views/admin/CardKeysPage.vue @@ -116,8 +116,8 @@ async function loadCardKeys() { loading.value = true try { const response = await getCardKeys({ page: currentPage.value, page_size: pageSize.value }) - if (response.data.success && response.data.keys) { - cardKeys.value = response.data.keys + if (response.data.success) { + cardKeys.value = response.data.keys || [] total.value = response.data.total || 0 } else { toast.error(response.data.message || '获取卡密列表失败') diff --git a/frontend/src/views/admin/TeamsPage.vue b/frontend/src/views/admin/TeamsPage.vue index 3c58711..941bb57 100644 --- a/frontend/src/views/admin/TeamsPage.vue +++ b/frontend/src/views/admin/TeamsPage.vue @@ -54,7 +54,8 @@ import { } from '@/components/ui/pagination' import { useAccountsStore } from '@/stores/accounts' import { createAccount, refreshAccount, deleteAccount, type Account } from '@/api/accounts' -import { Plus, RefreshCw, Users, Loader2, Eye, EyeOff, Trash2 } from 'lucide-vue-next' +import { inviteByAdmin } from '@/api/invite' +import { Plus, RefreshCw, Users, Loader2, Eye, EyeOff, Trash2, UserPlus, Shuffle } from 'lucide-vue-next' const router = useRouter() const accountsStore = useAccountsStore() @@ -67,6 +68,18 @@ const showToken = ref(false) // Delete confirmation const deleteDialogOpen = ref(false) + +// Invite dialog +const inviteDialogOpen = ref(false) +const invitingAccountId = ref(null) +const invitingAccountName = ref('') +const inviteEmail = ref('') +const inviting = ref(false) + +// Random invite dialog +const randomInviteDialogOpen = ref(false) +const randomInviteEmail = ref('') +const randomInviting = ref(false) const pendingDelete = ref(null) // Pagination @@ -185,6 +198,74 @@ function viewInvites(account: Account) { router.push(`/admin/teams/${account.id}/invites`) } +function openInviteDialog(account: Account) { + invitingAccountId.value = account.id + invitingAccountName.value = account.name || account.team_account_id + inviteEmail.value = '' + inviteDialogOpen.value = true +} + +async function handleInvite() { + if (!inviteEmail.value.trim()) { + toast.error('请输入邮箱地址') + return + } + if (!invitingAccountId.value) return + + inviting.value = true + try { + const response = await inviteByAdmin({ + email: inviteEmail.value.trim(), + account_id: invitingAccountId.value, + }) + if (response.data.success) { + toast.success('邀请发送成功') + inviteDialogOpen.value = false + inviteEmail.value = '' + // Refresh account to update seats + if (invitingAccountId.value) { + await handleRefresh(accountsStore.accounts.find(a => a.id === invitingAccountId.value)!) + } + } else { + toast.error(response.data.message || '邀请失败') + } + } catch (e: any) { + toast.error(e.response?.data?.message || '邀请失败') + } finally { + inviting.value = false + } +} + +// Random invite - auto select available team +async function handleRandomInvite() { + if (!randomInviteEmail.value.trim()) { + toast.error('请输入邮箱地址') + return + } + + randomInviting.value = true + try { + // Use account_id = 0 to let backend auto-select + const response = await inviteByAdmin({ + email: randomInviteEmail.value.trim(), + account_id: 0, + }) + if (response.data.success) { + toast.success(`邀请发送成功,已分配到: ${response.data.account_name || 'Team'}`) + randomInviteDialogOpen.value = false + randomInviteEmail.value = '' + // Refresh all accounts to update seats + await accountsStore.fetchAccounts() + } else { + toast.error(response.data.message || '邀请失败') + } + } catch (e: any) { + toast.error(e.response?.data?.message || '邀请失败') + } finally { + randomInviting.value = false + } +} + function goToPage(page: number) { if (page >= 1 && page <= totalPages.value) { currentPage.value = page @@ -203,6 +284,7 @@ function handlePageSizeChange(value: any) {

Team 管理

+
+ + + + + + + + + 随机邀请 + + 系统将自动选择有空位的 Team 发送邀请 + + +
+
+ + +
+ + + +
+
+
+
@@ -329,7 +448,10 @@ function handlePageSizeChange(value: any) { ]" /> - + + + + +