Files
member-frontend/src/pages/admin/ApiClientsPage.vue

283 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-800">API Clients</h2>
<div class="flex gap-2">
<el-button type="primary" @click="openCreate">新增 Client</el-button>
<el-button :loading="loading" @click="load" :icon="Refresh" size="small">重新整理</el-button>
</div>
</div>
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
<el-table :data="items" stripe border class="w-full shadow-sm" v-loading="loading">
<template #empty><el-empty description="目前無 API Client" /></template>
<el-table-column prop="client_key" label="Client Key" min-width="180" />
<el-table-column prop="name" label="名稱" min-width="160" />
<el-table-column label="狀態" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'info'">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Allowed Paths" min-width="220">
<template #default="{ row }">{{ (row.allowed_paths || []).join(', ') || '-' }}</template>
</el-table-column>
<el-table-column prop="last_used_at" label="最後使用" min-width="170" />
<el-table-column prop="expires_at" label="到期日" min-width="170" />
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
<el-button size="small" type="warning" @click="handleRotate(row)">重置 Key</el-button>
<el-button size="small" :type="row.status === 'active' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === 'active' ? '停用' : '啟用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="showCreateDialog" title="新增 API Client" width="700px" @close="resetCreate">
<el-form ref="createFormRef" :model="createForm" :rules="rules" label-width="130px">
<el-form-item label="名稱" prop="name"><el-input v-model="createForm.name" /></el-form-item>
<el-form-item label="Client Key">
<el-input v-model="createForm.client_key" placeholder="留空自動產生" />
</el-form-item>
<el-form-item label="Allowed Origins">
<el-input v-model="createAllowedOrigins" placeholder="逗號分隔,例如 https://erp.ose.tw" />
</el-form-item>
<el-form-item label="Allowed IPs">
<el-input v-model="createAllowedIps" placeholder="逗號分隔,例如 10.0.0.1,10.0.0.2" />
</el-form-item>
<el-form-item label="Allowed Paths">
<el-input v-model="createAllowedPaths" placeholder="逗號分隔,例如 /internal/" />
</el-form-item>
<el-form-item label="Rate Limit/min">
<el-input-number v-model="createForm.rate_limit_per_min" :min="0" :step="10" />
</el-form-item>
<el-form-item label="到期日">
<el-date-picker v-model="createExpiresAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss[Z]" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreate">建立</el-button>
</template>
</el-dialog>
<el-dialog v-model="showEditDialog" title="編輯 API Client" width="700px" @close="resetEdit">
<el-form :model="editForm" label-width="130px">
<el-form-item label="Client Key"><el-input :model-value="editForm.client_key" disabled /></el-form-item>
<el-form-item label="名稱"><el-input v-model="editForm.name" /></el-form-item>
<el-form-item label="狀態">
<el-select v-model="editForm.status">
<el-option label="active" value="active" />
<el-option label="inactive" value="inactive" />
</el-select>
</el-form-item>
<el-form-item label="Allowed Origins">
<el-input v-model="editAllowedOrigins" placeholder="逗號分隔" />
</el-form-item>
<el-form-item label="Allowed IPs">
<el-input v-model="editAllowedIps" placeholder="逗號分隔" />
</el-form-item>
<el-form-item label="Allowed Paths">
<el-input v-model="editAllowedPaths" placeholder="逗號分隔" />
</el-form-item>
<el-form-item label="Rate Limit/min">
<el-input-number v-model="editForm.rate_limit_per_min" :min="0" :step="10" />
</el-form-item>
<el-form-item label="到期日">
<el-date-picker v-model="editExpiresAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss[Z]" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditDialog = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">儲存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { createApiClient, getApiClients, rotateApiClientKey, updateApiClient } from '@/api/api-clients'
const items = ref([])
const loading = ref(false)
const error = ref(false)
const errorMsg = ref('')
const showCreateDialog = ref(false)
const showEditDialog = ref(false)
const creating = ref(false)
const saving = ref(false)
const createFormRef = ref()
const rules = {
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
}
const createForm = ref({
name: '',
client_key: '',
rate_limit_per_min: null
})
const createAllowedOrigins = ref('')
const createAllowedIps = ref('')
const createAllowedPaths = ref('/internal/')
const createExpiresAt = ref(null)
const editForm = ref({
client_key: '',
name: '',
status: 'active',
rate_limit_per_min: null
})
const editAllowedOrigins = ref('')
const editAllowedIps = ref('')
const editAllowedPaths = ref('')
const editExpiresAt = ref(null)
function toList(value) {
return String(value || '')
.split(',')
.map(v => v.trim())
.filter(Boolean)
}
function resetCreate() {
createForm.value = { name: '', client_key: '', rate_limit_per_min: null }
createAllowedOrigins.value = ''
createAllowedIps.value = ''
createAllowedPaths.value = '/internal/'
createExpiresAt.value = null
}
function resetEdit() {
editForm.value = { client_key: '', name: '', status: 'active', rate_limit_per_min: null }
editAllowedOrigins.value = ''
editAllowedIps.value = ''
editAllowedPaths.value = ''
editExpiresAt.value = null
}
function openCreate() {
resetCreate()
showCreateDialog.value = true
}
function openEdit(row) {
editForm.value = {
client_key: row.client_key,
name: row.name,
status: row.status,
rate_limit_per_min: row.rate_limit_per_min
}
editAllowedOrigins.value = (row.allowed_origins || []).join(', ')
editAllowedIps.value = (row.allowed_ips || []).join(', ')
editAllowedPaths.value = (row.allowed_paths || []).join(', ')
editExpiresAt.value = row.expires_at
showEditDialog.value = true
}
async function load() {
loading.value = true
error.value = false
try {
const res = await getApiClients()
items.value = res.data?.items || []
} catch (err) {
error.value = true
errorMsg.value = err.response?.data?.detail || '載入失敗'
} finally {
loading.value = false
}
}
async function handleCreate() {
const valid = await createFormRef.value.validate().catch(() => false)
if (!valid) return
creating.value = true
try {
const payload = {
name: createForm.value.name,
client_key: createForm.value.client_key || null,
allowed_origins: toList(createAllowedOrigins.value),
allowed_ips: toList(createAllowedIps.value),
allowed_paths: toList(createAllowedPaths.value),
rate_limit_per_min: createForm.value.rate_limit_per_min,
expires_at: createExpiresAt.value
}
const res = await createApiClient(payload)
const apiKey = res.data?.api_key || ''
if (apiKey) {
await navigator.clipboard.writeText(apiKey)
ElMessage.success(`建立成功API Key 已複製:${apiKey}`)
} else {
ElMessage.success('建立成功')
}
showCreateDialog.value = false
await load()
} catch (err) {
ElMessage.error(err.response?.data?.detail || '建立失敗')
} finally {
creating.value = false
}
}
async function handleSave() {
saving.value = true
try {
const payload = {
name: editForm.value.name,
status: editForm.value.status,
allowed_origins: toList(editAllowedOrigins.value),
allowed_ips: toList(editAllowedIps.value),
allowed_paths: toList(editAllowedPaths.value),
rate_limit_per_min: editForm.value.rate_limit_per_min,
expires_at: editExpiresAt.value
}
await updateApiClient(editForm.value.client_key, payload)
ElMessage.success('更新成功')
showEditDialog.value = false
await load()
} catch (err) {
ElMessage.error(err.response?.data?.detail || '更新失敗')
} finally {
saving.value = false
}
}
async function handleRotate(row) {
try {
await ElMessageBox.confirm(`確定要重置 ${row.client_key} 的 API Key 嗎?`, '重置確認', { type: 'warning' })
const res = await rotateApiClientKey(row.client_key)
const apiKey = res.data?.api_key || ''
if (apiKey) {
await navigator.clipboard.writeText(apiKey)
ElMessage.success(`重置成功API Key 已複製:${apiKey}`)
} else {
ElMessage.success('重置成功')
}
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '重置失敗')
}
}
async function toggleStatus(row) {
try {
const next = row.status === 'active' ? 'inactive' : 'active'
await updateApiClient(row.client_key, { status: next })
ElMessage.success(next === 'active' ? '已啟用' : '已停用')
await load()
} catch (err) {
ElMessage.error(err.response?.data?.detail || '更新狀態失敗')
}
}
onMounted(load)
</script>