Done
This commit is contained in:
29
frontend/src/components/CfTurnstile.vue
Normal file
29
frontend/src/components/CfTurnstile.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="cf-turnstile">
|
||||
<div ref="turnstileWidget"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const turnstileWidget = ref(null)
|
||||
const emit = defineEmits(['verify'])
|
||||
|
||||
onMounted(() => {
|
||||
if (window.turnstile) {
|
||||
window.turnstile.render(turnstileWidget.value, {
|
||||
sitekey: 'your-site-key',
|
||||
callback: (token) => {
|
||||
emit('verify', token)
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cf-turnstile {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
</style>
|
33
frontend/src/components/NavBar.vue
Normal file
33
frontend/src/components/NavBar.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<nav class="navbar">
|
||||
<router-link to="/">首页</router-link>
|
||||
<router-link to="/user/dashboard">用户面板</router-link>
|
||||
<router-link to="/admin/dashboard" v-if="isAdmin">管理面板</router-link>
|
||||
<button @click="logout">退出</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const isAdmin = computed(() => authStore.user?.role === 'admin')
|
||||
|
||||
const logout = () => {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navbar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
</style>
|
9
frontend/src/main.js
Normal file
9
frontend/src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
18
frontend/src/package.json
Normal file
18
frontend/src/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "firewall-frontend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"pinia": "^2.0.33",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.1.4"
|
||||
}
|
||||
}
|
23
frontend/src/router/index.js
Normal file
23
frontend/src/router/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/user/dashboard'
|
||||
},
|
||||
{
|
||||
path: '/user/dashboard',
|
||||
component: () => import('../views/User/Dashboard.vue')
|
||||
},
|
||||
{
|
||||
path: '/admin/dashboard',
|
||||
component: () => import('../views/Admin/Dashboard.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
44
frontend/src/stores/auth.js
Normal file
44
frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref(null)
|
||||
const router = useRouter()
|
||||
|
||||
// 登录
|
||||
const login = async (username, password) => {
|
||||
const response = await axios.post('/api/login', {
|
||||
username,
|
||||
password
|
||||
})
|
||||
user.value = response.data.user
|
||||
localStorage.setItem('token', response.data.token)
|
||||
router.push('/user/dashboard')
|
||||
}
|
||||
|
||||
// 登出
|
||||
const logout = () => {
|
||||
user.value = null
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/user/me')
|
||||
user.value = response.data
|
||||
} catch (error) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
checkAuth
|
||||
}
|
||||
})
|
27
frontend/src/stores/requests.js
Normal file
27
frontend/src/stores/requests.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export const useRequestsStore = defineStore('requests', () => {
|
||||
const pendingRequests = ref([])
|
||||
const approvedRequests = ref([])
|
||||
|
||||
// 获取待审批请求
|
||||
const fetchPendingRequests = async () => {
|
||||
const response = await axios.get('/api/admin/requests')
|
||||
pendingRequests.value = response.data
|
||||
}
|
||||
|
||||
// 获取已批准请求
|
||||
const fetchApprovedRequests = async () => {
|
||||
const response = await axios.get('/api/user/requests')
|
||||
approvedRequests.value = response.data
|
||||
}
|
||||
|
||||
return {
|
||||
pendingRequests,
|
||||
approvedRequests,
|
||||
fetchPendingRequests,
|
||||
fetchApprovedRequests
|
||||
}
|
||||
})
|
43
frontend/src/views/Admin/Blacklist.vue
Normal file
43
frontend/src/views/Admin/Blacklist.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="blacklist">
|
||||
<h2>黑名单管理</h2>
|
||||
<div v-if="blacklist.length > 0">
|
||||
<ul>
|
||||
<li v-for="item in blacklist" :key="item.id">
|
||||
{{ item.target }} - {{ item.type }}
|
||||
<button @click="removeFromBlacklist(item.id)">移除</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<input v-model="newTarget" placeholder="输入IP或用户名">
|
||||
<button @click="addToBlacklist">加入黑名单</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const blacklist = ref([])
|
||||
const newTarget = ref('')
|
||||
|
||||
const fetchBlacklist = async () => {
|
||||
const response = await axios.get('/api/admin/blacklist')
|
||||
blacklist.value = response.data
|
||||
}
|
||||
|
||||
const addToBlacklist = async () => {
|
||||
await axios.post('/api/admin/blacklist', { target: newTarget.value })
|
||||
newTarget.value = ''
|
||||
await fetchBlacklist()
|
||||
}
|
||||
|
||||
const removeFromBlacklist = async (id) => {
|
||||
await axios.delete(`/api/admin/blacklist/${id}`)
|
||||
await fetchBlacklist()
|
||||
}
|
||||
|
||||
onMounted(fetchBlacklist)
|
||||
</script>
|
139
frontend/src/views/Admin/Dashboard.vue
Normal file
139
frontend/src/views/Admin/Dashboard.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="admin-dashboard">
|
||||
<h2>管理员面板</h2>
|
||||
<div v-if="pendingRequests.length > 0">
|
||||
<h3>待审批请求</h3>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户</th>
|
||||
<th>IP 地址</th>
|
||||
<th>申请时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="req in pendingRequests" :key="req.id">
|
||||
<td>{{ req.username }}</td>
|
||||
<td>{{ req.ip }}</td>
|
||||
<td>{{ formatDate(req.created_at) }}</td>
|
||||
<td>
|
||||
<button @click="approveRequest(req.id)" class="approve-btn">批准</button>
|
||||
<button @click="rejectRequest(req.id)" class="reject-btn">拒绝</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>没有待审批的请求。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
// 待审批请求列表
|
||||
const pendingRequests = ref([])
|
||||
|
||||
// 获取待审批请求
|
||||
const fetchPendingRequests = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/admin/requests')
|
||||
pendingRequests.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取待审批请求失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 批准请求
|
||||
const approveRequest = async (requestId) => {
|
||||
try {
|
||||
await axios.post('/api/admin/approve', { request_id: requestId })
|
||||
await fetchPendingRequests()
|
||||
alert('请求已批准')
|
||||
} catch (error) {
|
||||
console.error('批准请求失败:', error)
|
||||
alert('操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝请求
|
||||
const rejectRequest = async (requestId) => {
|
||||
try {
|
||||
await axios.post('/api/admin/reject', { request_id: requestId })
|
||||
await fetchPendingRequests()
|
||||
alert('请求已拒绝')
|
||||
} catch (error) {
|
||||
console.error('拒绝请求失败:', error)
|
||||
alert('操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
// 组件加载时获取数据
|
||||
onMounted(fetchPendingRequests)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-dashboard {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.requests-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.requests-table th,
|
||||
.requests-table td {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.requests-table th {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.requests-table tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.approve-btn {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.approve-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.reject-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reject-btn:hover {
|
||||
background-color: #e53935;
|
||||
}
|
||||
</style>
|
32
frontend/src/views/User/Dashboard.vue
Normal file
32
frontend/src/views/User/Dashboard.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h2>用户面板</h2>
|
||||
<div v-if="requests.length > 0">
|
||||
<h3>我的请求</h3>
|
||||
<ul>
|
||||
<li v-for="req in requests" :key="req.id">
|
||||
{{ req.ip }} - {{ req.status }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button @click="requestNewIP">申请新IP</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRequestsStore } from '../stores/requests'
|
||||
|
||||
const requestsStore = useRequestsStore()
|
||||
const requests = ref([])
|
||||
|
||||
const requestNewIP = async () => {
|
||||
await axios.post('/api/user/request')
|
||||
await requestsStore.fetchApprovedRequests()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await requestsStore.fetchApprovedRequests()
|
||||
requests.value = requestsStore.approvedRequests
|
||||
})
|
||||
</script>
|
26
frontend/src/views/User/RequestForm.vue
Normal file
26
frontend/src/views/User/RequestForm.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="request-form">
|
||||
<h3>申请新IP</h3>
|
||||
<form @submit.prevent="submitRequest">
|
||||
<CfTurnstile @verify="setToken" />
|
||||
<button type="submit" :disabled="!token">提交申请</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import CfTurnstile from '../components/CfTurnstile.vue'
|
||||
|
||||
const token = ref('')
|
||||
|
||||
const setToken = (newToken) => {
|
||||
token.value = newToken
|
||||
}
|
||||
|
||||
const submitRequest = async () => {
|
||||
await axios.post('/api/user/request', { token: token.value })
|
||||
alert('申请已提交')
|
||||
}
|
||||
</script>
|
14
frontend/src/vite.config.js
Normal file
14
frontend/src/vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Reference in New Issue
Block a user