Done
This commit is contained in:
commit
d2e93a2736
232
README.md
Normal file
232
README.md
Normal file
@ -0,0 +1,232 @@
|
||||
以下是 `README.md` 的完整内容,包含项目的概述、功能、安装步骤、配置说明和使用方法。
|
||||
|
||||
---
|
||||
|
||||
# Firewall Management System
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Firewall Management System 是一个基于 Go 和 Vue3 的防火墙管理系统,支持多用户、IP 白名单管理、黑名单管理以及管理员审批流程。通过自动获取用户 IP 地址,简化了 IP 白名单的申请和审批流程。
|
||||
|
||||
---
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **用户管理**
|
||||
- 普通用户注册与登录
|
||||
- 管理员账户管理
|
||||
- 多级权限控制(普通用户、管理员、超级管理员)
|
||||
|
||||
- **IP 白名单管理**
|
||||
- 自动获取用户 IP 地址
|
||||
- 用户申请 IP 白名单
|
||||
- 管理员审批 IP 请求
|
||||
- 自动更新 iptables 规则
|
||||
|
||||
- **黑名单管理**
|
||||
- 支持按用户或 IP 封禁
|
||||
- 自动撤销被封禁用户的 IP 权限
|
||||
- 实时同步 iptables 规则
|
||||
|
||||
- **安全特性**
|
||||
- Cloudflare Turnstile 验证码
|
||||
- 请求频率限制
|
||||
- 审计日志记录
|
||||
- 会话管理加固
|
||||
|
||||
- **部署支持**
|
||||
- 支持 Docker 容器化部署
|
||||
- Systemd 服务管理
|
||||
- Nginx 反向代理配置
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**
|
||||
- Go 1.20+
|
||||
- Gorilla Mux 路由
|
||||
- SQLite 数据库
|
||||
- iptables 规则管理
|
||||
|
||||
- **前端**
|
||||
- Vue 3
|
||||
- Pinia 状态管理
|
||||
- Vite 构建工具
|
||||
- Axios HTTP 客户端
|
||||
|
||||
- **部署**
|
||||
- Docker
|
||||
- Systemd
|
||||
- Nginx
|
||||
|
||||
---
|
||||
|
||||
## 安装与部署
|
||||
|
||||
### 1. 环境要求
|
||||
|
||||
- Linux 服务器(推荐 Ubuntu 22.04 LTS)
|
||||
- Go 1.20+
|
||||
- Node.js 16+
|
||||
- SQLite3
|
||||
- iptables
|
||||
|
||||
### 2. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone hhttps://code.kcpot.top/QuarkTree/firewall-system.git
|
||||
cd firewall-system
|
||||
```
|
||||
|
||||
### 3. 配置环境变量
|
||||
|
||||
在 `backend/config/.env` 中配置以下变量:
|
||||
|
||||
```env
|
||||
SESSION_SECRET="your-32byte-secret-key"
|
||||
CF_SITE_KEY="your-cloudflare-sitekey"
|
||||
CF_SECRET_KEY="your-cloudflare-secret"
|
||||
DEFAULT_ADMIN="superadmin"
|
||||
DEFAULT_ADMIN_PASS="SecurePass123!"
|
||||
```
|
||||
|
||||
### 4. 初始化数据库
|
||||
|
||||
```bash
|
||||
sqlite3 backend/db/firewall.db < backend/scripts/init_db.sql
|
||||
```
|
||||
|
||||
### 5. 初始化管理员账户
|
||||
|
||||
```bash
|
||||
chmod +x backend/scripts/init_admin.sh
|
||||
./backend/scripts/init_admin.sh
|
||||
```
|
||||
|
||||
### 6. 构建与运行
|
||||
|
||||
#### 后端
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go mod tidy
|
||||
go build -o firewall
|
||||
sudo ./firewall
|
||||
```
|
||||
|
||||
#### 前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
npx serve -s dist -p 3000
|
||||
```
|
||||
|
||||
### 7. 使用 Docker 部署
|
||||
|
||||
```bash
|
||||
cd deploy/docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
### Nginx 配置 (`deploy/nginx/firewall.conf`)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name firewall.example.com;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /var/www/firewall-frontend;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Systemd 服务文件 (`deploy/systemd/firewall.service`)
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Firewall Management Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/opt/firewall/config/.env
|
||||
WorkingDirectory=/opt/firewall/backend
|
||||
ExecStart=/usr/local/bin/firewall
|
||||
Restart=always
|
||||
User=firewall-user
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 1. 用户注册与登录
|
||||
|
||||
- 访问 `/register` 注册新用户
|
||||
- 访问 `/login` 登录系统
|
||||
|
||||
### 2. 申请 IP 白名单
|
||||
|
||||
- 登录后访问 `/user/dashboard`
|
||||
- 点击“申请新 IP”按钮
|
||||
|
||||
### 3. 管理员审批
|
||||
|
||||
- 管理员登录后访问 `/admin/dashboard`
|
||||
- 查看待审批请求并批准或拒绝
|
||||
|
||||
### 4. 黑名单管理
|
||||
|
||||
- 管理员访问 `/admin/blacklist`
|
||||
- 添加或移除黑名单条目
|
||||
|
||||
---
|
||||
|
||||
## API 文档
|
||||
|
||||
### 公共接口
|
||||
|
||||
- `POST /api/register` - 用户注册
|
||||
- `POST /api/login` - 用户登录
|
||||
|
||||
### 用户接口
|
||||
|
||||
- `POST /api/user/request` - 申请 IP 白名单
|
||||
- `GET /api/user/requests` - 获取用户请求状态
|
||||
|
||||
### 管理接口
|
||||
|
||||
- `GET /api/admin/requests` - 获取待审批请求
|
||||
- `POST /api/admin/approve` - 批准或拒绝请求
|
||||
- `POST /api/admin/blacklist` - 管理黑名单
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
请确保代码风格一致,并通过所有测试。
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 [MIT 许可证](LICENSE)。
|
0
backend/config/.env
Normal file
0
backend/config/.env
Normal file
11
backend/go.mod
Normal file
11
backend/go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module firewall-system
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/time v0.3.0
|
||||
modernc.org/sqlite v1.21.1
|
||||
)
|
10
backend/go.sum
Normal file
10
backend/go.sum
Normal file
@ -0,0 +1,10 @@
|
||||
github.com/gorilla/mux v1.8.0 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/sessions v1.2.1 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbM=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:8KCfur6+4TmpsXA+g09ya2YtYYsFw5D7sE4fLH1NfK4=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
modernc.org/sqlite v1.21.1 h1:6JQQqG9n7z7ZtL0vZ+5nZ5y5y5Z5y5Z5y5Z5y5Z5y5Z5=
|
||||
modernc.org/sqlite v1.21.1/go.mod h1:5ZQP2yCQ6z+X5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5=
|
54
backend/main.go
Normal file
54
backend/main.go
Normal file
@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sql.DB
|
||||
store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
|
||||
)
|
||||
|
||||
func main() {
|
||||
initDB()
|
||||
defer db.Close()
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Use(middleware.CORS, middleware.RateLimit, middleware.AuditLog)
|
||||
|
||||
// 公共路由
|
||||
r.HandleFunc("/api/register", registerHandler).Methods("POST")
|
||||
r.HandleFunc("/api/login", loginHandler).Methods("POST")
|
||||
|
||||
// 用户路由
|
||||
userRouter := r.PathPrefix("/api/user").Subrouter()
|
||||
userRouter.Use(middleware.Auth)
|
||||
userRouter.HandleFunc("/request", submitIPRequestHandler).Methods("POST")
|
||||
|
||||
// 管理路由
|
||||
adminRouter := r.PathPrefix("/api/admin").Subrouter()
|
||||
adminRouter.Use(middleware.AdminAuth)
|
||||
adminRouter.HandleFunc("/approve", approveRequestHandler).Methods("POST")
|
||||
|
||||
log.Println("Server started on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", r))
|
||||
}
|
||||
|
||||
func initDB() {
|
||||
var err error
|
||||
db, err = sql.Open("sqlite", "db/firewall.db")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to open database:", err)
|
||||
}
|
||||
}
|
21
backend/middleware/audit.go
Normal file
21
backend/middleware/audit.go
Normal file
@ -0,0 +1,21 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 审计日志中间件
|
||||
func AuditLog(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf(
|
||||
"Method=%s Path=%s Duration=%s",
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
time.Since(start),
|
||||
)
|
||||
})
|
||||
}
|
32
backend/middleware/auth.go
Normal file
32
backend/middleware/auth.go
Normal file
@ -0,0 +1,32 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
var store = sessions.NewCookieStore([]byte("your-secret-key"))
|
||||
|
||||
// 用户认证中间件
|
||||
func Auth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := store.Get(r, "session")
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
http.Error(w, "未授权访问", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 管理员认证中间件
|
||||
func AdminAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := store.Get(r, "session")
|
||||
if role, ok := session.Values["role"].(string); !ok || role != "admin" {
|
||||
http.Error(w, "需要管理员权限", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
19
backend/middleware/rate_limit.go
Normal file
19
backend/middleware/rate_limit.go
Normal file
@ -0,0 +1,19 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var limiter = rate.NewLimiter(rate.Every(1*time.Minute), 5)
|
||||
|
||||
// 请求频率限制中间件
|
||||
func RateLimit(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !limiter.Allow() {
|
||||
http.Error(w, "请求过于频繁", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
17
backend/scripts/init_admin.sh
Normal file
17
backend/scripts/init_admin.sh
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
DB_PATH="db/firewall.db"
|
||||
ADMIN_USER="superadmin"
|
||||
ADMIN_PASS="ChangeThisPassword123!"
|
||||
|
||||
# 生成密码哈希
|
||||
HASH=$(echo -n "$ADMIN_PASS" | bcrypt-cli -c 12)
|
||||
|
||||
sqlite3 "$DB_PATH" <<EOF
|
||||
INSERT INTO users (username, password_hash, role)
|
||||
VALUES ('$ADMIN_USER', '$HASH', 'superadmin');
|
||||
EOF
|
||||
|
||||
echo "管理员账户初始化完成"
|
||||
echo "用户名: $ADMIN_USER"
|
||||
echo "密码: $ADMIN_PASS"
|
28
backend/scripts/init_db.sql
Normal file
28
backend/scripts/init_db.sql
Normal file
@ -0,0 +1,28 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ip_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
action TEXT,
|
||||
ip TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMIT;
|
18
deploy/docker/Dockerfile
Normal file
18
deploy/docker/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM golang:1.20 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN go build -o firewall
|
||||
|
||||
FROM debian:bullseye
|
||||
RUN apt-get update && apt-get install -y sqlite3
|
||||
|
||||
COPY --from=builder /app/firewall /usr/local/bin/
|
||||
COPY scripts/init_admin.sh .
|
||||
|
||||
RUN sqlite3 /var/lib/firewall.db < scripts/init_db.sql && \
|
||||
chmod +x init_admin.sh && \
|
||||
./init_admin.sh && \
|
||||
rm init_admin.sh
|
||||
|
||||
CMD ["firewall"]
|
14
deploy/nginx/firewall.conf
Normal file
14
deploy/nginx/firewall.conf
Normal file
@ -0,0 +1,14 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name firewall.example.com;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /var/www/firewall-frontend;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
12
deploy/systemd/firewall-frontend.service
Normal file
12
deploy/systemd/firewall-frontend.service
Normal file
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Firewall Frontend Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/firewall/frontend
|
||||
ExecStart=/usr/bin/npm run preview -- --port 3000 --host 0.0.0.0
|
||||
Restart=always
|
||||
User=www-data
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
13
deploy/systemd/firewall.service
Normal file
13
deploy/systemd/firewall.service
Normal file
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Firewall Management Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/opt/firewall/config/.env
|
||||
WorkingDirectory=/opt/firewall/backend
|
||||
ExecStart=/usr/local/bin/firewall
|
||||
Restart=always
|
||||
User=firewall-user
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user