- Add user management with roles and permissions (RBAC) - Implement OAuth2 service provider supporting 4 grant types: authorization_code, password, client_credentials, refresh_token - Add JWT authentication with 7-day expiry - Add admin API for users, roles and OAuth clients management - Add CLI tool for user management (scripts/user-cli.js) - Add collapsible sidebar layout with login dialog - Add user management page and OAuth client management page - Add server middleware for auth token verification - Add seed script for initial data (admin/admin123)
355 lines
9.4 KiB
Vue
355 lines
9.4 KiB
Vue
<template>
|
|
<div class="oauth-container">
|
|
<el-card>
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>OAuth 客户端管理</span>
|
|
<el-button type="primary" @click="showAddDialog = true">创建客户端</el-button>
|
|
</div>
|
|
</template>
|
|
|
|
<el-table :data="clients" border stripe v-loading="loading">
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|
<el-table-column prop="clientName" label="应用名称" />
|
|
<el-table-column prop="clientId" label="Client ID" width="200">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="row.clientId" placement="top">
|
|
<code class="client-id">{{ row.clientId }}</code>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="platform" label="平台" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag>{{ getPlatformText(row.platform) }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="grantTypes" label="授权模式" width="200">
|
|
<template #default="{ row }">
|
|
<el-tag
|
|
v-for="grant in row.grantTypes"
|
|
:key="grant"
|
|
size="small"
|
|
class="grant-tag"
|
|
>
|
|
{{ getGrantText(grant) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="isActive" label="状态" width="100">
|
|
<template #default="{ row }">
|
|
<el-switch
|
|
v-model="row.isActive"
|
|
@change="toggleStatus(row)"
|
|
:disabled="row.platform === 'web'"
|
|
/>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="createdAt" label="创建时间" width="180">
|
|
<template #default="{ row }">
|
|
{{ new Date(row.createdAt).toLocaleString('zh-CN') }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="120" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button type="danger" size="small" @click="deleteClient(row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<el-dialog v-model="showAddDialog" title="创建 OAuth 客户端" width="600px">
|
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
|
<el-form-item label="应用名称" prop="clientName">
|
|
<el-input v-model="form.clientName" placeholder="请输入应用名称" />
|
|
</el-form-item>
|
|
<el-form-item label="平台" prop="platform">
|
|
<el-radio-group v-model="form.platform">
|
|
<el-radio value="web">网页应用</el-radio>
|
|
<el-radio value="mobile">移动应用</el-radio>
|
|
<el-radio value="desktop">桌面应用</el-radio>
|
|
<el-radio value="other">其他</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
<el-form-item label="授权模式" prop="grantTypes">
|
|
<el-checkbox-group v-model="form.grantTypes">
|
|
<el-checkbox value="authorization_code">授权码模式</el-checkbox>
|
|
<el-checkbox value="password">密码模式</el-checkbox>
|
|
<el-checkbox value="client_credentials">客户端凭据模式</el-checkbox>
|
|
<el-checkbox value="refresh_token">刷新令牌</el-checkbox>
|
|
</el-checkbox-group>
|
|
</el-form-item>
|
|
<el-form-item label="重定向 URI" prop="redirectUris">
|
|
<el-input
|
|
v-model="redirectUrisText"
|
|
type="textarea"
|
|
:rows="3"
|
|
placeholder="多个 URI 用换行分隔"
|
|
/>
|
|
<div class="form-tip">每行一个 URI</div>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="showAddDialog = false">取消</el-button>
|
|
<el-button type="primary" @click="submitForm">创建</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<el-dialog v-model="showSecretDialog" title="Client Secret" width="500px">
|
|
<el-alert
|
|
title="请妥善保存 Client Secret"
|
|
type="warning"
|
|
description="Client Secret 只会显示一次,请立即复制保存"
|
|
:closable="false"
|
|
show-icon
|
|
/>
|
|
<div class="secret-display">
|
|
<p><strong>Client ID:</strong></p>
|
|
<code class="secret-value">{{ newClient.clientId }}</code>
|
|
<p><strong>Client Secret:</strong></p>
|
|
<code class="secret-value">{{ newClient.clientSecret }}</code>
|
|
<el-button type="primary" class="copy-btn" @click="copySecret">复制到剪贴板</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({
|
|
layout: 'default'
|
|
})
|
|
|
|
const clients = ref<any[]>([])
|
|
const loading = ref(false)
|
|
const showAddDialog = ref(false)
|
|
const showSecretDialog = ref(false)
|
|
const redirectUrisText = ref('')
|
|
const formRef = ref()
|
|
|
|
const form = ref({
|
|
clientName: '',
|
|
platform: 'web',
|
|
grantTypes: ['authorization_code'] as string[]
|
|
})
|
|
|
|
const newClient = ref({
|
|
clientId: '',
|
|
clientSecret: ''
|
|
})
|
|
|
|
const rules = {
|
|
clientName: [
|
|
{ required: true, message: '请输入应用名称', trigger: 'blur' }
|
|
],
|
|
grantTypes: [
|
|
{ required: true, message: '请至少选择一种授权模式', trigger: 'change' }
|
|
]
|
|
}
|
|
|
|
const getPlatformText = (platform: string) => {
|
|
const texts: Record<string, string> = {
|
|
web: '网页',
|
|
mobile: '移动',
|
|
desktop: '桌面',
|
|
other: '其他'
|
|
}
|
|
return texts[platform] || platform
|
|
}
|
|
|
|
const getGrantText = (grant: string) => {
|
|
const texts: Record<string, string> = {
|
|
authorization_code: '授权码',
|
|
password: '密码',
|
|
client_credentials: '客户端',
|
|
refresh_token: '刷新'
|
|
}
|
|
return texts[grant] || grant
|
|
}
|
|
|
|
const loadClients = async () => {
|
|
const token = localStorage.getItem('token')
|
|
if (!token) {
|
|
ElMessage.warning('请先登录')
|
|
navigateTo('/')
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
try {
|
|
const res = await $fetch('/api/admin/oauth', {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
clients.value = res.data
|
|
} catch (error: any) {
|
|
ElMessage.error(error.data?.message || '加载失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const toggleStatus = async (client: any) => {
|
|
const token = localStorage.getItem('token')
|
|
if (!token) return
|
|
|
|
try {
|
|
await $fetch(`/api/admin/oauth?clientId=${client.clientId}`, {
|
|
method: 'PUT',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
body: { isActive: client.isActive }
|
|
})
|
|
ElMessage.success('状态更新成功')
|
|
} catch (error: any) {
|
|
ElMessage.error(error.data?.message || '更新失败')
|
|
loadClients()
|
|
}
|
|
}
|
|
|
|
const deleteClient = async (client: any) => {
|
|
const token = localStorage.getItem('token')
|
|
if (!token) return
|
|
|
|
try {
|
|
await ElMessageBox.confirm(`确定删除客户端 "${client.clientName}" 吗?`, '提示', {
|
|
type: 'warning'
|
|
})
|
|
|
|
await $fetch(`/api/admin/oauth?clientId=${client.clientId}`, {
|
|
method: 'DELETE',
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
ElMessage.success('删除成功')
|
|
loadClients()
|
|
} catch (error: any) {
|
|
if (error !== 'cancel') {
|
|
ElMessage.error(error.data?.message || '删除失败')
|
|
}
|
|
}
|
|
}
|
|
|
|
const submitForm = async () => {
|
|
if (!formRef.value) return
|
|
|
|
await formRef.value.validate(async (valid: boolean) => {
|
|
if (!valid) return
|
|
|
|
const token = localStorage.getItem('token')
|
|
if (!token) return
|
|
|
|
const redirectUris = redirectUrisText.value
|
|
.split('\n')
|
|
.map(uri => uri.trim())
|
|
.filter(uri => uri)
|
|
|
|
if (redirectUris.length === 0) {
|
|
ElMessage.error('请至少输入一个重定向 URI')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const res = await $fetch('/api/admin/oauth', {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
body: {
|
|
...form.value,
|
|
redirectUris,
|
|
allowedScopes: ['read', 'write']
|
|
}
|
|
})
|
|
|
|
newClient.value = {
|
|
clientId: res.data.clientId,
|
|
clientSecret: res.data.clientSecret
|
|
}
|
|
|
|
showAddDialog.value = false
|
|
showSecretDialog.value = true
|
|
resetForm()
|
|
loadClients()
|
|
} catch (error: any) {
|
|
ElMessage.error(error.data?.message || '创建失败')
|
|
}
|
|
})
|
|
}
|
|
|
|
const resetForm = () => {
|
|
form.value = {
|
|
clientName: '',
|
|
platform: 'web',
|
|
grantTypes: ['authorization_code']
|
|
}
|
|
redirectUrisText.value = ''
|
|
formRef.value?.resetFields()
|
|
}
|
|
|
|
const copySecret = () => {
|
|
const text = `Client ID: ${newClient.value.clientId}\nClient Secret: ${newClient.value.clientSecret}`
|
|
navigator.clipboard.writeText(text)
|
|
ElMessage.success('已复制到剪贴板')
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadClients()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.oauth-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.client-id {
|
|
font-size: 12px;
|
|
color: #409eff;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.grant-tag {
|
|
margin-right: 4px;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.form-tip {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.secret-display {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.secret-display p {
|
|
margin: 16px 0 8px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.secret-value {
|
|
display: block;
|
|
padding: 12px;
|
|
background-color: #f5f7fa;
|
|
border-radius: 4px;
|
|
word-break: break-all;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.copy-btn {
|
|
margin-top: 20px;
|
|
width: 100%;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.card-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
}
|
|
</style>
|