Skip to main content
Glama
AuthManager.vue36.8 kB
<template> <div class="auth-manager"> <!-- 页面头部 --> <div class="header-section"> <div class="header-content"> <h1> <el-icon><Lock /></el-icon> 认证配置管理 </h1> <p class="header-description"> 配置和管理API认证信息,支持多种认证方式的安全存储和测试 </p> </div> <div class="header-actions"> <el-button type="primary" :icon="Plus" @click="showCreateDialog"> 添加认证配置 </el-button> <el-button :icon="Refresh" @click="refreshData" :loading="loading"> 刷新 </el-button> </div> </div> <!-- 过期警告 --> <div v-if="expiredConfigs.length > 0" class="expired-warning"> <el-alert title="认证配置过期提醒" type="warning" show-icon :closable="false" > <template #default> <div> 以下认证配置可能已过期,建议重新测试或更新: <ul class="expired-list"> <li v-for="config in expiredConfigs" :key="config.id"> {{ getConfigDisplayName(config) }} ({{ config.type.toUpperCase() }}) <el-button size="small" text @click="testConfig(config.id)"> 重新测试 </el-button> </li> </ul> </div> </template> </el-alert> </div> <!-- 主要内容区域 --> <div class="main-content"> <!-- 管理工具栏 --> <div class="management-toolbar"> <div class="toolbar-section"> <h4>快速操作</h4> <div class="toolbar-actions"> <el-button type="primary" @click="authStore.startExpirationMonitoring()" :icon="Connection" > 启动过期监控 </el-button> <el-button type="warning" @click="authStore.checkAllExpiration()" :icon="Refresh" > 检查过期状态 </el-button> <el-button type="danger" @click="confirmCleanupExpired" :icon="Delete" :disabled="expiredConfigs.length === 0" > 清理过期配置 ({{ expiredConfigs.length }}) </el-button> </div> </div> <div class="toolbar-section"> <h4>统计信息</h4> <div class="stats-grid"> <div class="stat-item"> <div class="stat-value">{{ authStore.configList.length }}</div> <div class="stat-label">总配置数</div> </div> <div class="stat-item"> <div class="stat-value">{{ activeConfig ? 1 : 0 }}</div> <div class="stat-label">活跃配置</div> </div> <div class="stat-item"> <div class="stat-value">{{ expiredConfigs.length }}</div> <div class="stat-label">已过期</div> </div> <div class="stat-item"> <div class="stat-value">{{ getExpiringSoonCount() }}</div> <div class="stat-label">即将过期</div> </div> </div> </div> </div> <!-- 认证配置列表 --> <div class="config-list-section"> <div class="section-header"> <h3>认证配置列表</h3> <div class="header-filters"> <el-select v-model="selectedType" placeholder="筛选认证类型" clearable style="width: 150px" > <el-option label="Bearer Token" value="bearer" /> <el-option label="API Key" value="apikey" /> <el-option label="Basic Auth" value="basic" /> <el-option label="OAuth2" value="oauth2" /> </el-select> </div> </div> <div class="config-cards" v-loading="loading"> <div v-for="config in filteredConfigs" :key="config.id" class="config-card" :class="{ active: activeConfigId === config.id, expired: checkAuthExpiration(config.id), }" > <div class="card-header"> <div class="config-info"> <div class="config-name"> {{ getConfigDisplayName(config) }} </div> <el-tag :type="getTypeColor(config.type)"> {{ getTypeLabel(config.type) }} </el-tag> </div> <div class="card-actions"> <el-tooltip content="设为活跃"> <el-button size="small" :type="activeConfigId === config.id ? 'primary' : 'default'" :icon="activeConfigId === config.id ? Check : Plus" @click="setActiveConfig(config.id)" circle /> </el-tooltip> <el-tooltip content="测试连接"> <el-button size="small" :icon="Connection" @click="testConfig(config.id)" :loading="testing === config.id" circle /> </el-tooltip> <el-tooltip content="编辑"> <el-button size="small" :icon="Edit" @click="editConfig(config)" circle /> </el-tooltip> <el-tooltip content="删除"> <el-button size="small" type="danger" :icon="Delete" @click="deleteConfig(config.id)" circle /> </el-tooltip> </div> </div> <div class="card-content"> <!-- 认证信息摘要 --> <div class="credentials-summary"> <div v-if="config.type === 'bearer'" class="credential-item"> <span class="label">Token:</span> <span class="value">{{ maskCredential(config.credentials.token) }}</span> </div> <div v-else-if="config.type === 'apikey'" class="credential-item" > <span class="label">API Key:</span> <span class="value">{{ maskCredential(config.credentials.apiKey) }}</span> </div> <div v-else-if="config.type === 'basic'"> <div class="credential-item"> <span class="label">用户名:</span> <span class="value">{{ config.credentials.username }}</span> </div> <div class="credential-item"> <span class="label">密码:</span> <span class="value">{{ maskCredential(config.credentials.password) }}</span> </div> </div> <div v-else-if="config.type === 'oauth2'"> <div class="credential-item"> <span class="label">Client ID:</span> <span class="value">{{ config.credentials.clientId }}</span> </div> <div class="credential-item"> <span class="label">Client Secret:</span> <span class="value">{{ maskCredential(config.credentials.clientSecret) }}</span> </div> </div> </div> <!-- 环境变量 --> <div v-if="config.envVars && config.envVars.length > 0" class="env-vars" > <div class="env-label">环境变量:</div> <el-tag v-for="envVar in config.envVars" :key="envVar" size="small" style="margin-right: 4px; margin-bottom: 4px" > {{ envVar }} </el-tag> </div> <!-- 测试结果 --> <div v-if="testResults.get(config.id)" class="test-result"> <div class="result-header"> <span class="result-label">最近测试:</span> <el-tag :type=" testResults.get(config.id)?.success ? 'success' : 'danger' " size="small" > {{ testResults.get(config.id)?.success ? "成功" : "失败" }} </el-tag> <span class="result-time"> {{ formatDateTime( testResults.get(config.id)?.timestamp || new Date(), ) }} </span> </div> <div class="result-message"> {{ testResults.get(config.id)?.message }} </div> </div> </div> </div> <!-- 空状态 --> <div v-if="filteredConfigs.length === 0" class="empty-state"> <el-empty description="暂无认证配置"> <el-button type="primary" @click="showCreateDialog"> 创建第一个认证配置 </el-button> </el-empty> </div> </div> </div> </div> <!-- 创建/编辑认证配置对话框 --> <el-dialog v-model="createDialogVisible" :title="editingConfig ? '编辑认证配置' : '创建认证配置'" width="600px" :close-on-click-modal="false" > <el-form ref="authFormRef" :model="authForm" label-width="120px" :rules="authFormRules" > <el-form-item label="配置名称" prop="name"> <el-input v-model="authForm.name" placeholder="输入认证配置名称" /> </el-form-item> <el-form-item label="认证类型" prop="type"> <el-select v-model="authForm.type" placeholder="选择认证类型" style="width: 100%" @change="handleTypeChange" > <el-option label="Bearer Token" value="bearer"> <div class="option-content"> <div class="option-title">Bearer Token</div> <div class="option-desc">使用Bearer令牌进行认证</div> </div> </el-option> <el-option label="API Key" value="apikey"> <div class="option-content"> <div class="option-title">API Key</div> <div class="option-desc">使用API密钥进行认证</div> </div> </el-option> <el-option label="Basic Auth" value="basic"> <div class="option-content"> <div class="option-title">Basic Auth</div> <div class="option-desc">使用用户名密码进行基础认证</div> </div> </el-option> <el-option label="OAuth2" value="oauth2"> <div class="option-content"> <div class="option-title">OAuth2</div> <div class="option-desc">使用OAuth2客户端凭据流</div> </div> </el-option> </el-select> </el-form-item> <!-- Bearer Token 配置 --> <template v-if="authForm.type === 'bearer'"> <el-form-item label="Token" prop="credentials.token"> <el-input v-model="authForm.credentials.token" type="password" placeholder="输入Bearer Token" show-password /> </el-form-item> </template> <!-- API Key 配置 --> <template v-if="authForm.type === 'apikey'"> <el-form-item label="API Key" prop="credentials.apiKey"> <el-input v-model="authForm.credentials.apiKey" type="password" placeholder="输入API Key" show-password /> </el-form-item> </template> <!-- Basic Auth 配置 --> <template v-if="authForm.type === 'basic'"> <el-form-item label="用户名" prop="credentials.username"> <el-input v-model="authForm.credentials.username" placeholder="输入用户名" /> </el-form-item> <el-form-item label="密码" prop="credentials.password"> <el-input v-model="authForm.credentials.password" type="password" placeholder="输入密码" show-password /> </el-form-item> </template> <!-- OAuth2 配置 --> <template v-if="authForm.type === 'oauth2'"> <el-form-item label="Client ID" prop="credentials.clientId"> <el-input v-model="authForm.credentials.clientId" placeholder="输入Client ID" /> </el-form-item> <el-form-item label="Client Secret" prop="credentials.clientSecret"> <el-input v-model="authForm.credentials.clientSecret" type="password" placeholder="输入Client Secret" show-password /> </el-form-item> </template> <!-- 环境变量配置 --> <el-form-item label="环境变量"> <div class="env-vars-config"> <div class="env-input-group"> <el-input v-model="newEnvVar" placeholder="输入环境变量名称" @keyup.enter="addEnvVar" /> <el-button @click="addEnvVar" :disabled="!newEnvVar.trim()"> 添加 </el-button> <el-button @click="validateEnvVars" :loading="validatingEnvVars"> 验证环境变量 </el-button> </div> <div class="env-vars-list" v-if="authForm.envVars.length > 0"> <el-tag v-for="(envVar, index) in authForm.envVars" :key="index" :type="getEnvVarStatus(envVar)" closable @close="removeEnvVar(index)" style="margin: 4px 4px 4px 0" > {{ envVar }} <el-icon v-if="getEnvVarStatus(envVar) === 'success'" ><Check /></el-icon> <el-icon v-else-if="getEnvVarStatus(envVar) === 'danger'" ><Close /></el-icon> </el-tag> </div> <div v-if="envVarValidationResult" class="env-validation-result"> <div v-if="envVarValidationResult.available.length > 0" class="available-vars" > <el-text type="success"> <el-icon><Check /></el-icon> 可用环境变量: {{ envVarValidationResult.available.join(", ") }} </el-text> </div> <div v-if="envVarValidationResult.missing.length > 0" class="missing-vars" > <el-text type="danger"> <el-icon><Close /></el-icon> 缺失环境变量: {{ envVarValidationResult.missing.join(", ") }} </el-text> </div> </div> <div class="env-suggestions"> <span class="suggestions-label">常用环境变量:</span> <el-button v-for="suggestion in envVarSuggestions" :key="suggestion" size="small" text @click="quickAddEnvVar(suggestion)" > {{ suggestion }} </el-button> </div> </div> </el-form-item> <!-- 自动续期设置 --> <el-form-item label="自动续期"> <div class="auto-renewal-config"> <el-switch v-model="authForm.autoRenewal" active-text="启用自动续期提醒" /> <div v-if="authForm.autoRenewal" class="renewal-settings"> <el-form-item label="提醒间隔" style="margin-bottom: 8px"> <el-select v-model="authForm.renewalInterval" style="width: 100%" > <el-option label="1小时" :value="1" /> <el-option label="6小时" :value="6" /> <el-option label="12小时" :value="12" /> <el-option label="24小时" :value="24" /> <el-option label="7天" :value="168" /> </el-select> </el-form-item> </div> </div> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="createDialogVisible = false">取消</el-button> <el-button @click="testFormConfig" :loading="testing === 'form'"> 测试连接 </el-button> <el-button type="primary" @click="saveAuthConfig" :loading="saving"> {{ editingConfig ? "更新" : "创建" }} </el-button> </div> </template> </el-dialog> <!-- 测试详情对话框 --> <el-dialog v-model="testDetailsDialogVisible" title="认证测试详情" width="500px" > <div v-if="selectedTestResult" class="test-details"> <div class="detail-item"> <strong>测试状态:</strong> <el-tag :type="selectedTestResult.success ? 'success' : 'danger'"> {{ selectedTestResult.success ? "成功" : "失败" }} </el-tag> </div> <div class="detail-item"> <strong>测试时间:</strong> {{ formatDateTime(selectedTestResult.timestamp) }} </div> <div class="detail-item"> <strong>测试消息:</strong> {{ selectedTestResult.message }} </div> <div v-if="selectedTestResult.details" class="detail-item"> <strong>详细信息:</strong> <el-input type="textarea" :value="JSON.stringify(selectedTestResult.details, null, 2)" readonly :rows="6" /> </div> </div> </el-dialog> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted, onUnmounted } from "vue"; import { ElMessage, ElMessageBox, type FormInstance, type FormRules, } from "element-plus"; import { Lock, Plus, Refresh, Check, Close, Connection, Edit, Delete, } from "@element-plus/icons-vue"; import { useAuthStore } from "@/stores/auth"; import type { AuthConfig, AuthTestResult } from "@/types"; // Store const authStore = useAuthStore(); // Reactive data const loading = ref(false); const saving = ref(false); const testing = ref<string | null>(null); const selectedType = ref(""); // 对话框状态 const createDialogVisible = ref(false); const testDetailsDialogVisible = ref(false); const selectedTestResult = ref<AuthTestResult | null>(null); // 表单数据 const authFormRef = ref<FormInstance>(); const editingConfig = ref<({ id: string } & AuthConfig) | null>(null); const authForm = ref({ name: "", type: "" as AuthConfig["type"], credentials: { token: "", apiKey: "", username: "", password: "", clientId: "", clientSecret: "", }, envVars: [] as string[], autoRenewal: false, renewalInterval: 24, }); // 环境变量管理 const newEnvVar = ref(""); const validatingEnvVars = ref(false); const envVarValidationResult = ref<{ available: string[]; missing: string[]; } | null>(null); const envVarSuggestions = [ "API_TOKEN", "SECRET_KEY", "CLIENT_ID", "CLIENT_SECRET", "BEARER_TOKEN", "API_KEY", "USERNAME", "PASSWORD", ]; // Computed properties const filteredConfigs = computed(() => { if (!selectedType.value) return authStore.configList; return authStore.configList.filter( (config) => config.type === selectedType.value, ); }); const expiredConfigs = computed(() => { return authStore.getExpiredConfigs(); }); const activeConfigId = computed(() => authStore.activeConfigId); const activeConfig = computed(() => authStore.activeConfig); const testResults = computed(() => authStore.testResults); // 表单验证规则 const authFormRules: FormRules = { name: [ { required: true, message: "请输入配置名称", trigger: "blur" }, { min: 2, max: 50, message: "长度在 2 到 50 个字符", trigger: "blur" }, ], type: [{ required: true, message: "请选择认证类型", trigger: "change" }], "credentials.token": [ { required: true, message: "请输入Bearer Token", trigger: "blur" }, ], "credentials.apiKey": [ { required: true, message: "请输入API Key", trigger: "blur" }, ], "credentials.username": [ { required: true, message: "请输入用户名", trigger: "blur" }, ], "credentials.password": [ { required: true, message: "请输入密码", trigger: "blur" }, ], "credentials.clientId": [ { required: true, message: "请输入Client ID", trigger: "blur" }, ], "credentials.clientSecret": [ { required: true, message: "请输入Client Secret", trigger: "blur" }, ], }; // Methods const showCreateDialog = () => { editingConfig.value = null; resetForm(); createDialogVisible.value = true; }; const editConfig = (config: { id: string } & AuthConfig) => { editingConfig.value = config; // 加载配置数据到表单 const decryptedCredentials = authStore.getDecryptedCredentials(config.id) || {}; authForm.value = { name: getConfigDisplayName(config), type: config.type, credentials: { token: decryptedCredentials.token || "", apiKey: decryptedCredentials.apiKey || "", username: decryptedCredentials.username || "", password: decryptedCredentials.password || "", clientId: decryptedCredentials.clientId || "", clientSecret: decryptedCredentials.clientSecret || "", }, envVars: [...(config.envVars || [])], autoRenewal: false, renewalInterval: 24, }; createDialogVisible.value = true; }; const resetForm = () => { authForm.value = { name: "", type: "" as AuthConfig["type"], credentials: { token: "", apiKey: "", username: "", password: "", clientId: "", clientSecret: "", }, envVars: [] as string[], autoRenewal: false, renewalInterval: 24, }; }; const handleTypeChange = () => { // 清空凭据字段 authForm.value.credentials = { token: "", apiKey: "", username: "", password: "", clientId: "", clientSecret: "", }; }; const addEnvVar = () => { const envVar = newEnvVar.value.trim(); if (envVar && !authForm.value.envVars.includes(envVar)) { authForm.value.envVars.push(envVar); newEnvVar.value = ""; } }; const removeEnvVar = (index: number) => { authForm.value.envVars.splice(index, 1); }; const quickAddEnvVar = (envVar: string) => { if (!authForm.value.envVars.includes(envVar)) { authForm.value.envVars.push(envVar); } }; const testFormConfig = async () => { if (!authFormRef.value) return; const valid = await authFormRef.value.validate().catch(() => false); if (!valid) { ElMessage.error("请检查表单输入"); return; } testing.value = "form"; try { // 创建临时配置进行测试 const tempConfig: Omit<AuthConfig, "encrypted"> = { type: authForm.value.type, credentials: { ...authForm.value.credentials }, envVars: [...authForm.value.envVars], }; // 模拟测试 const result = await authStore.testAuthConfig("temp"); selectedTestResult.value = result; testDetailsDialogVisible.value = true; } catch (error) { ElMessage.error("测试失败"); } finally { testing.value = null; } }; const saveAuthConfig = async () => { if (!authFormRef.value) return; const valid = await authFormRef.value.validate().catch(() => false); if (!valid) return; saving.value = true; try { const configData: Omit<AuthConfig, "encrypted"> = { type: authForm.value.type, credentials: { ...authForm.value.credentials }, envVars: [...authForm.value.envVars], }; if (editingConfig.value) { // 更新现有配置 authStore.updateAuthConfig(editingConfig.value.id, configData); } else { // 创建新配置 authStore.createAuthConfig(authForm.value.name, configData); } createDialogVisible.value = false; resetForm(); } catch (error) { ElMessage.error("保存失败"); } finally { saving.value = false; } }; const testConfig = async (configId: string) => { testing.value = configId; try { const result = await authStore.testAuthConfig(configId); if (result.details) { selectedTestResult.value = result; testDetailsDialogVisible.value = true; } } finally { testing.value = null; } }; const deleteConfig = async (configId: string) => { try { await ElMessageBox.confirm( "确定要删除这个认证配置吗?这将安全清除所有相关的敏感信息。", "确认删除", { type: "warning", confirmButtonText: "删除", cancelButtonText: "取消", }, ); await authStore.deleteAuthConfig(configId); } catch (error) { // 用户取消删除 } }; const setActiveConfig = (configId: string) => { authStore.setActiveConfig( authStore.activeConfigId === configId ? null : configId, ); }; const checkAuthExpiration = (configId: string): boolean => { return authStore.checkAuthExpiration(configId); }; const refreshData = async () => { loading.value = true; try { await authStore.loadAvailableEnvVars(); ElMessage.success("数据刷新成功"); } catch (error) { ElMessage.error("数据刷新失败"); } finally { loading.value = false; } }; // 辅助函数 const getConfigDisplayName = (config: { id: string } & AuthConfig): string => { // 从ID中提取时间戳作为名称 const timestamp = config.id.split("_")[1]; const date = new Date(parseInt(timestamp)); return `${getTypeLabel(config.type)} 配置 (${date.toLocaleDateString()})`; }; const getTypeLabel = (type: AuthConfig["type"]): string => { const labels = { bearer: "Bearer Token", apikey: "API Key", basic: "Basic Auth", oauth2: "OAuth2", }; return labels[type]; }; const getTypeColor = (type: AuthConfig["type"]) => { const colors = { bearer: "primary", apikey: "success", basic: "warning", oauth2: "info", }; return colors[type]; }; const maskCredential = (credential?: string): string => { if (!credential) return "未设置"; if (credential.length <= 8) return "*".repeat(credential.length); return ( credential.substring(0, 4) + "*".repeat(credential.length - 8) + credential.substring(credential.length - 4) ); }; const formatDateTime = (date: Date): string => { return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }).format(new Date(date)); }; // 环境变量验证方法 const validateEnvVars = async () => { if (authForm.value.envVars.length === 0) { ElMessage.warning("请先添加环境变量"); return; } validatingEnvVars.value = true; try { // 模拟环境变量检查 const available: string[] = []; const missing: string[] = []; for (const envVar of authForm.value.envVars) { // 在实际环境中,这里应该调用API检查环境变量 // 现在我们模拟一些常见的环境变量为可用状态 const commonVars = [ "API_TOKEN", "SECRET_KEY", "CLIENT_ID", "BEARER_TOKEN", ]; if (commonVars.includes(envVar) || Math.random() > 0.3) { available.push(envVar); } else { missing.push(envVar); } } envVarValidationResult.value = { available, missing }; if (missing.length === 0) { ElMessage.success("所有环境变量均可用"); } else { ElMessage.warning(`发现 ${missing.length} 个缺失的环境变量`); } } catch (error) { ElMessage.error("环境变量验证失败"); } finally { validatingEnvVars.value = false; } }; const getEnvVarStatus = (envVar: string): string => { if (!envVarValidationResult.value) return ""; if (envVarValidationResult.value.available.includes(envVar)) { return "success"; } else if (envVarValidationResult.value.missing.includes(envVar)) { return "danger"; } return ""; }; // 过期管理相关方法 const getExpiringSoonCount = (): number => { const now = Date.now(); return authStore.configList.filter((config) => { if (!config.expiresAt) return false; const timeLeft = new Date(config.expiresAt).getTime() - now; const hoursLeft = timeLeft / (1000 * 60 * 60); return hoursLeft > 0 && hoursLeft <= 24; }).length; }; const confirmCleanupExpired = async () => { if (expiredConfigs.value.length === 0) { ElMessage.info("没有过期的认证配置"); return; } try { await ElMessageBox.confirm( `确定要清理 ${expiredConfigs.value.length} 个过期的认证配置吗?此操作不可撤销。`, "确认清理", { confirmButtonText: "确定清理", cancelButtonText: "取消", type: "warning", dangerouslyUseHTMLString: false, }, ); await authStore.cleanupExpiredConfigs(); } catch (error) { // 用户取消操作 if (error === "cancel") { return; } ElMessage.error("清理操作失败"); } }; // 生命周期 onMounted(async () => { loading.value = true; try { await authStore.loadAvailableEnvVars(); // 启动过期监控 authStore.startExpirationMonitoring(); } finally { loading.value = false; } }); onUnmounted(() => { // 清理过期监控定时器 authStore.stopExpirationMonitoring(); }); </script> <style scoped> .auth-manager { height: 100%; display: flex; flex-direction: column; padding: 20px; background-color: var(--el-bg-color-page); } .header-section { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--el-border-color-light); } .header-content h1 { margin: 0 0 8px 0; color: var(--el-text-color-primary); display: flex; align-items: center; gap: 8px; font-size: 24px; font-weight: 600; } .header-description { margin: 0; color: var(--el-text-color-regular); font-size: 14px; } .header-actions { display: flex; gap: 12px; } .expired-warning { margin-bottom: 20px; } .expired-list { margin: 8px 0 0 20px; } .expired-list li { margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between; } .main-content { flex: 1; overflow-y: auto; } .management-toolbar { background: var(--el-bg-color); border: 1px solid var(--el-border-color-light); border-radius: 6px; padding: 16px; margin-bottom: 20px; display: grid; grid-template-columns: 1fr 300px; gap: 20px; } .toolbar-section h4 { margin: 0 0 12px 0; color: var(--el-text-color-primary); font-size: 14px; font-weight: 600; } .toolbar-actions { display: flex; gap: 8px; flex-wrap: wrap; } .stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; } .stat-item { text-align: center; padding: 8px; background: var(--el-fill-color-lighter); border-radius: 4px; } .stat-value { font-size: 20px; font-weight: 600; color: var(--el-color-primary); margin-bottom: 4px; } .stat-label { font-size: 12px; color: var(--el-text-color-secondary); } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .section-header h3 { margin: 0; color: var(--el-text-color-primary); font-size: 18px; font-weight: 600; } .config-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 16px; } .config-card { background: white; border-radius: 8px; padding: 16px; border: 1px solid var(--el-border-color-light); transition: all 0.2s; } .config-card:hover { box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); } .config-card.active { border-color: var(--el-color-primary); box-shadow: 0 0 0 2px var(--el-color-primary-light-8); } .config-card.expired { border-color: var(--el-color-warning); background-color: var(--el-color-warning-light-9); } .card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; } .config-info { flex: 1; } .config-name { font-weight: 600; color: var(--el-text-color-primary); margin-bottom: 4px; } .card-actions { display: flex; gap: 4px; } .card-content { margin-top: 12px; } .credentials-summary { margin-bottom: 12px; } .credential-item { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 14px; } .credential-item .label { color: var(--el-text-color-secondary); font-weight: 500; } .credential-item .value { color: var(--el-text-color-primary); font-family: monospace; } .env-vars { margin-bottom: 12px; } .env-label { font-size: 12px; color: var(--el-text-color-secondary); margin-bottom: 4px; } .test-result { padding-top: 8px; border-top: 1px solid var(--el-border-color-lighter); } .result-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .result-label { font-size: 12px; color: var(--el-text-color-secondary); } .result-time { font-size: 11px; color: var(--el-text-color-placeholder); margin-left: auto; } .result-message { font-size: 12px; color: var(--el-text-color-regular); } .empty-state { grid-column: 1 / -1; text-align: center; padding: 40px; } .option-content { display: flex; flex-direction: column; } .option-title { font-weight: 600; color: var(--el-text-color-primary); } .option-desc { font-size: 12px; color: var(--el-text-color-secondary); } .env-vars-config { width: 100%; } .env-input-group { display: flex; gap: 8px; margin-bottom: 8px; } .env-input-group .el-input { flex: 1; } .env-vars-list { margin-bottom: 12px; padding: 8px; background-color: var(--el-bg-color-page); border-radius: 4px; border: 1px solid var(--el-border-color-lighter); } .env-validation-result { margin: 8px 0; padding: 8px; border-radius: 4px; background-color: var(--el-bg-color-page); } .available-vars, .missing-vars { margin: 4px 0; display: flex; align-items: center; gap: 4px; } .env-suggestions { margin-top: 8px; padding: 8px; background-color: var(--el-fill-color-lighter); border-radius: 4px; } .suggestions-label { font-size: 12px; color: var(--el-text-color-secondary); margin-right: 8px; } .auto-renewal-config { width: 100%; } .renewal-settings { margin-top: 12px; padding: 12px; background-color: var(--el-fill-color-lighter); border-radius: 4px; } .env-vars-list { margin-bottom: 8px; } .env-suggestions { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--el-border-color-lighter); } .suggestions-label { font-size: 12px; color: var(--el-text-color-secondary); margin-right: 8px; } .test-details .detail-item { margin-bottom: 16px; } .test-details .detail-item strong { display: block; margin-bottom: 4px; color: var(--el-text-color-primary); } @media (max-width: 768px) { .config-cards { grid-template-columns: 1fr; } .header-section { flex-direction: column; gap: 12px; align-items: stretch; } .card-header { flex-direction: column; gap: 8px; } .card-actions { align-self: flex-end; } } </style>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zaizaizhao/mcp-swagger-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server