├── self-hosted ├── frontend │ ├── vue.config.js │ ├── src │ │ ├── store │ │ │ ├── index.js │ │ │ └── modules │ │ │ │ ├── auth.js │ │ │ │ └── domains.js │ │ ├── main.js │ │ ├── App.vue │ │ ├── utils │ │ │ ├── api.js │ │ │ └── helpers.js │ │ ├── router │ │ │ └── index.js │ │ └── views │ │ │ ├── Login.vue │ │ │ ├── Dashboard.vue │ │ │ ├── Domains.vue │ │ │ └── Admin.vue │ ├── .eslintrc.js │ ├── Dockerfile │ ├── public │ │ └── index.html │ ├── package.json │ └── nginx.conf ├── backend │ ├── Dockerfile │ ├── .env.example │ ├── package.json │ ├── middleware │ │ └── auth.js │ ├── routes │ │ ├── whois.js │ │ ├── auth.js │ │ └── domains.js │ ├── app.js │ ├── services │ │ ├── whoisService.js │ │ ├── cloudflareService.js │ │ └── syncService.js │ └── utils │ │ └── database.js ├── .env.example ├── docker-compose.yml ├── deploy.sh ├── README-zh.md └── README.md ├── PROJECT_OVERVIEW.md ├── .gitignore ├── index.js ├── README.md └── domainkeeper.js /self-hosted/frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | port: 3000, 4 | proxy: { 5 | '/api': { 6 | target: 'http://localhost:3001', 7 | changeOrigin: true, 8 | ws: true 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import auth from './modules/auth' 3 | import domains from './modules/domains' 4 | 5 | export default createStore({ 6 | state: { 7 | }, 8 | getters: { 9 | }, 10 | mutations: { 11 | }, 12 | actions: { 13 | }, 14 | modules: { 15 | auth, 16 | domains 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /self-hosted/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # 后端 Dockerfile 2 | FROM node:18-alpine 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 复制package文件 8 | COPY package*.json ./ 9 | 10 | # 安装依赖 11 | RUN npm install --only=production 12 | 13 | # 复制源代码 14 | COPY . . 15 | 16 | # 创建必要的目录 17 | RUN mkdir -p logs data 18 | 19 | # 暴露端口 20 | EXPOSE 3001 21 | 22 | # 启动应用 23 | CMD ["npm", "start"] 24 | -------------------------------------------------------------------------------- /self-hosted/frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2020 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'vue/multi-word-component-names': 'off', 17 | 'no-unused-vars': 'warn' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /self-hosted/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # 前端 Dockerfile 2 | FROM node:18-alpine as build-stage 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 复制package文件 8 | COPY package*.json ./ 9 | 10 | # 安装依赖 11 | RUN npm install 12 | 13 | # 复制源代码 14 | COPY . . 15 | 16 | # 构建应用 17 | RUN npm run build 18 | 19 | # 生产阶段 20 | FROM nginx:alpine as production-stage 21 | 22 | # 复制构建结果 23 | COPY --from=build-stage /app/dist /usr/share/nginx/html 24 | 25 | # 复制nginx配置 26 | COPY nginx.conf /etc/nginx/nginx.conf 27 | 28 | # 暴露端口 29 | EXPOSE 80 30 | 31 | # 启动nginx 32 | CMD ["nginx", "-g", "daemon off;"] 33 | -------------------------------------------------------------------------------- /self-hosted/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DomainKeeper - 域名管理系统 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import ElementPlus from 'element-plus' 6 | import 'element-plus/dist/index.css' 7 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 8 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 9 | 10 | const app = createApp(App) 11 | 12 | // 注册所有Element Plus图标 13 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 14 | app.component(key, component) 15 | } 16 | 17 | app.use(store) 18 | app.use(router) 19 | app.use(ElementPlus, { 20 | locale: zhCn, 21 | }) 22 | 23 | app.mount('#app') 24 | -------------------------------------------------------------------------------- /self-hosted/backend/.env.example: -------------------------------------------------------------------------------- 1 | # DomainKeeper 后端配置 2 | 3 | # 服务器配置 4 | PORT=3001 5 | NODE_ENV=production 6 | 7 | # JWT 密钥 (请务必更改为复杂的密钥) 8 | JWT_SECRET=your_super_secret_jwt_key_change_this_in_production 9 | 10 | # 管理员账户 11 | ADMIN_USERNAME=admin 12 | ADMIN_PASSWORD=your_admin_password 13 | 14 | # 前台访问密码 (可选,留空表示不需要密码) 15 | ACCESS_PASSWORD= 16 | 17 | # Cloudflare API 配置 18 | CF_API_TOKEN=your_cloudflare_api_token 19 | 20 | # WHOIS 代理服务地址 (可选) 21 | WHOIS_PROXY_URL=https://whois.0o11.com 22 | 23 | # 数据库文件路径 24 | DATABASE_PATH=./data/domains.db 25 | 26 | # 数据同步间隔 (分钟) 27 | SYNC_INTERVAL=60 28 | 29 | # 允许的前端域名 (CORS) 30 | FRONTEND_URL=http://localhost:3000 31 | 32 | # 自定义标题 33 | CUSTOM_TITLE=我的域名管理 34 | 35 | # 日志级别 36 | LOG_LEVEL=info 37 | -------------------------------------------------------------------------------- /self-hosted/.env.example: -------------------------------------------------------------------------------- 1 | # DomainKeeper 环境变量配置 2 | # 复制此文件为 .env 并根据您的需求修改配置 3 | 4 | # JWT 密钥 (必须修改) 5 | JWT_SECRET=your_super_secret_jwt_key_change_this_in_production 6 | 7 | # 管理员账户配置 8 | ADMIN_USERNAME=admin 9 | ADMIN_PASSWORD=admin123 10 | 11 | # 前台访问密码 (可选,留空表示不需要密码) 12 | ACCESS_PASSWORD= 13 | 14 | # Cloudflare API Token (可选,用于自动同步域名) 15 | CF_API_TOKEN= 16 | 17 | # WHOIS 代理服务地址 (可选,用于获取域名WHOIS信息) 18 | WHOIS_PROXY_URL=https://whois.0o11.com 19 | 20 | # 数据同步间隔 (分钟) 21 | SYNC_INTERVAL=60 22 | 23 | # 前端访问地址 (用于CORS配置) 24 | FRONTEND_URL=http://localhost 25 | 26 | # 前端端口 (默认80) 27 | FRONTEND_PORT=80 28 | 29 | # API 地址 (前端配置) 30 | VUE_APP_API_BASE_URL=http://localhost:3001/api 31 | 32 | # 自定义标题 33 | CUSTOM_TITLE=我的域名管理 34 | 35 | # 日志级别 (error, warn, info, debug) 36 | LOG_LEVEL=info 37 | -------------------------------------------------------------------------------- /self-hosted/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domainkeeper-backend", 3 | "version": "2.0.0", 4 | "description": "DomainKeeper 前后端分离版本 - 后端API服务", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon app.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "domain", 13 | "management", 14 | "cloudflare", 15 | "whois" 16 | ], 17 | "author": "bacon159", 18 | "license": "MIT", 19 | "dependencies": { 20 | "express": "^4.18.2", 21 | "cors": "^2.8.5", 22 | "helmet": "^7.1.0", 23 | "compression": "^1.7.4", 24 | "express-rate-limit": "^7.1.5", 25 | "bcryptjs": "^2.4.3", 26 | "jsonwebtoken": "^9.0.2", 27 | "axios": "^1.6.2", 28 | "dotenv": "^16.3.1", 29 | "sqlite3": "^5.1.6", 30 | "node-cron": "^3.0.3", 31 | "winston": "^3.11.0" 32 | }, 33 | "devDependencies": { 34 | "nodemon": "^3.0.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /self-hosted/backend/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | const authMiddleware = (requiredRole = null) => { 4 | return (req, res, next) => { 5 | const token = req.headers.authorization?.replace('Bearer ', ''); 6 | 7 | if (!token) { 8 | return res.status(401).json({ error: '未提供认证token' }); 9 | } 10 | 11 | try { 12 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 13 | 14 | // 检查角色权限 15 | if (requiredRole) { 16 | if (decoded.type === 'frontend' && requiredRole === 'admin') { 17 | return res.status(403).json({ error: '权限不足' }); 18 | } 19 | 20 | if (decoded.role && decoded.role !== requiredRole && requiredRole === 'admin') { 21 | return res.status(403).json({ error: '权限不足' }); 22 | } 23 | } 24 | 25 | req.user = decoded; 26 | next(); 27 | } catch (error) { 28 | if (error.name === 'JsonWebTokenError') { 29 | return res.status(401).json({ error: 'token无效' }); 30 | } else if (error.name === 'TokenExpiredError') { 31 | return res.status(401).json({ error: 'token已过期' }); 32 | } else { 33 | return res.status(401).json({ error: '认证失败' }); 34 | } 35 | } 36 | }; 37 | }; 38 | 39 | module.exports = authMiddleware; 40 | -------------------------------------------------------------------------------- /self-hosted/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domainkeeper-frontend", 3 | "version": "2.0.0", 4 | "private": true, 5 | "description": "DomainKeeper 前后端分离版本 - 前端应用", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "dev": "vue-cli-service serve --mode development" 11 | }, 12 | "dependencies": { 13 | "vue": "^3.3.0", 14 | "vue-router": "^4.2.0", 15 | "vuex": "^4.1.0", 16 | "axios": "^1.6.2", 17 | "element-plus": "^2.4.4", 18 | "@element-plus/icons-vue": "^2.1.0", 19 | "echarts": "^5.4.3", 20 | "vue-echarts": "^6.6.1", 21 | "dayjs": "^1.11.10" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.23.6", 25 | "@babel/eslint-parser": "^7.23.3", 26 | "@vue/cli-plugin-babel": "~5.0.0", 27 | "@vue/cli-plugin-eslint": "~5.0.0", 28 | "@vue/cli-plugin-router": "~5.0.0", 29 | "@vue/cli-plugin-vuex": "~5.0.0", 30 | "@vue/cli-service": "~5.0.0", 31 | "eslint": "^7.32.0", 32 | "eslint-plugin-vue": "^8.0.3" 33 | }, 34 | "eslintConfig": { 35 | "root": true, 36 | "env": { 37 | "node": true 38 | }, 39 | "extends": [ 40 | "plugin:vue/vue3-essential", 41 | "eslint:recommended" 42 | ], 43 | "parserOptions": { 44 | "parser": "@babel/eslint-parser", 45 | "requireConfigFile": false 46 | }, 47 | "rules": {} 48 | }, 49 | "browserslist": [ 50 | "> 1%", 51 | "last 2 versions", 52 | "not dead", 53 | "not ie 11" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 83 | -------------------------------------------------------------------------------- /self-hosted/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | # 后端服务 5 | backend: 6 | build: 7 | context: ./backend 8 | dockerfile: Dockerfile 9 | container_name: domainkeeper-backend 10 | restart: unless-stopped 11 | ports: 12 | - "3001:3001" 13 | environment: 14 | - NODE_ENV=production 15 | - PORT=3001 16 | - JWT_SECRET=${JWT_SECRET:-your_super_secret_jwt_key_change_this_in_production} 17 | - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} 18 | - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123} 19 | - ACCESS_PASSWORD=${ACCESS_PASSWORD:-} 20 | - CF_API_TOKEN=${CF_API_TOKEN:-} 21 | - WHOIS_PROXY_URL=${WHOIS_PROXY_URL:-} 22 | - DATABASE_PATH=/app/data/domains.db 23 | - SYNC_INTERVAL=${SYNC_INTERVAL:-60} 24 | - FRONTEND_URL=${FRONTEND_URL:-http://localhost} 25 | - CUSTOM_TITLE=${CUSTOM_TITLE:-我的域名管理} 26 | - LOG_LEVEL=${LOG_LEVEL:-info} 27 | volumes: 28 | - backend_data:/app/data 29 | - backend_logs:/app/logs 30 | networks: 31 | - domainkeeper 32 | 33 | # 前端服务 34 | frontend: 35 | build: 36 | context: ./frontend 37 | dockerfile: Dockerfile 38 | container_name: domainkeeper-frontend 39 | restart: unless-stopped 40 | ports: 41 | - "${FRONTEND_PORT:-80}:80" 42 | environment: 43 | - VUE_APP_API_BASE_URL=${VUE_APP_API_BASE_URL:-http://localhost:3001/api} 44 | depends_on: 45 | - backend 46 | networks: 47 | - domainkeeper 48 | 49 | volumes: 50 | backend_data: 51 | driver: local 52 | backend_logs: 53 | driver: local 54 | 55 | networks: 56 | domainkeeper: 57 | driver: bridge 58 | -------------------------------------------------------------------------------- /PROJECT_OVERVIEW.md: -------------------------------------------------------------------------------- 1 | # DomainKeeper 项目总览 2 | 3 | ## 📁 项目结构 4 | 5 | ``` 6 | domainkeeper/ 7 | ├── index.js # Workers 初级版本 8 | ├── domainkeeper.js # Workers 高级版本 9 | ├── README.md # Workers 版本说明文档 10 | └── self-hosted/ # 前后端分离版本 11 | ├── backend/ # Node.js 后端 12 | ├── frontend/ # Vue.js 前端 13 | ├── docker-compose.yml # Docker 编排文件 14 | ├── .env.example # 环境变量模板 15 | ├── deploy.sh # 一键部署脚本 16 | └── README.md # 自托管版本说明文档 17 | ``` 18 | 19 | ## 🚀 快速选择部署方案 20 | 21 | ### 场景一:个人使用,追求简单 22 | **推荐**: Cloudflare Workers 初级版 23 | ```bash 24 | # 1. 复制 index.js 内容到 Cloudflare Workers 25 | # 2. 修改域名列表 26 | # 3. 部署完成 27 | ``` 28 | 29 | ### 场景二:个人使用,需要自动同步 30 | **推荐**: Cloudflare Workers 高级版 31 | ```bash 32 | # 1. 复制 domainkeeper.js 内容到 Cloudflare Workers 33 | # 2. 配置 API Token 和 KV 存储 34 | # 3. 部署完成 35 | ``` 36 | 37 | ### 场景三:企业使用,功能完整 38 | **推荐**: 前后端分离版本 39 | ```bash 40 | cd self-hosted 41 | cp .env.example .env 42 | # 编辑配置文件 43 | docker-compose up -d 44 | ``` 45 | 46 | ## 📖 详细文档 47 | 48 | - **Cloudflare Workers 版本**: 查看 [README.md](./README.md) 49 | - **前后端分离版本**: 查看 [self-hosted/README.md](./self-hosted/README.md) 50 | 51 | ## 🔗 相关链接 52 | 53 | - **GitHub**: https://github.com/ypq123456789/domainkeeper 54 | - **演示地址**: http://demo.0o11.com 55 | - **WHOIS 代理**: https://github.com/ypq123456789/whois-proxy 56 | - **交流群组**: https://t.me/+ydvXl1_OBBBiZWM1 57 | 58 | ## 📝 版本历史 59 | 60 | - **v1.0**: 初级版本 (index.js) 61 | - **v1.1**: 高级版本 (domainkeeper.js) 62 | - **v2.0**: 前后端分离版本 (self-hosted/) 63 | 64 | ## 🤝 贡献 65 | 66 | 欢迎提交 Issues 和 Pull Requests! 67 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/utils/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { ElMessage } from 'element-plus' 3 | import router from '@/router' 4 | import store from '@/store' 5 | 6 | // 创建axios实例 7 | const api = axios.create({ 8 | baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3001/api', 9 | timeout: 30000, 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | } 13 | }) 14 | 15 | // 请求拦截器 16 | api.interceptors.request.use( 17 | config => { 18 | // 从store获取token 19 | const token = store.state.auth.token 20 | if (token) { 21 | config.headers.Authorization = `Bearer ${token}` 22 | } 23 | return config 24 | }, 25 | error => { 26 | return Promise.reject(error) 27 | } 28 | ) 29 | 30 | // 响应拦截器 31 | api.interceptors.response.use( 32 | response => { 33 | return response 34 | }, 35 | error => { 36 | if (error.response) { 37 | const { status, data } = error.response 38 | 39 | switch (status) { 40 | case 401: 41 | // 未授权,清除token并跳转到登录页 42 | store.dispatch('auth/logout') 43 | if (router.currentRoute.value.path !== '/login') { 44 | ElMessage.error('登录已过期,请重新登录') 45 | router.push('/login') 46 | } 47 | break 48 | case 403: 49 | ElMessage.error('权限不足') 50 | break 51 | case 404: 52 | ElMessage.error('请求的资源不存在') 53 | break 54 | case 500: 55 | ElMessage.error('服务器内部错误') 56 | break 57 | default: 58 | ElMessage.error(data.error || data.message || '请求失败') 59 | } 60 | } else if (error.request) { 61 | ElMessage.error('网络错误,请检查网络连接') 62 | } else { 63 | ElMessage.error('请求配置错误') 64 | } 65 | 66 | return Promise.reject(error) 67 | } 68 | ) 69 | 70 | export default api 71 | -------------------------------------------------------------------------------- /self-hosted/backend/routes/whois.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const whoisService = require('../services/whoisService'); 3 | const authMiddleware = require('../middleware/auth'); 4 | 5 | const router = express.Router(); 6 | 7 | // 查询WHOIS信息 8 | router.get('/:domain', authMiddleware, async (req, res) => { 9 | try { 10 | const { domain } = req.params; 11 | const { raw } = req.query; 12 | 13 | const whoisResult = await whoisService.fetchWhoisInfo(domain, Boolean(raw)); 14 | 15 | if (whoisResult.success) { 16 | res.json({ 17 | success: true, 18 | domain, 19 | data: whoisResult.data, 20 | rawData: whoisResult.rawData 21 | }); 22 | } else { 23 | res.status(500).json({ 24 | error: 'WHOIS查询失败', 25 | message: whoisResult.message 26 | }); 27 | } 28 | } catch (error) { 29 | console.error('WHOIS查询错误:', error); 30 | res.status(500).json({ error: 'WHOIS查询失败' }); 31 | } 32 | }); 33 | 34 | // 批量查询WHOIS信息 35 | router.post('/batch', authMiddleware('admin'), async (req, res) => { 36 | try { 37 | const { domains } = req.body; 38 | 39 | if (!Array.isArray(domains) || domains.length === 0) { 40 | return res.status(400).json({ error: '域名列表不能为空' }); 41 | } 42 | 43 | if (domains.length > 10) { 44 | return res.status(400).json({ error: '批量查询最多支持10个域名' }); 45 | } 46 | 47 | const results = await Promise.allSettled( 48 | domains.map(domain => whoisService.fetchWhoisInfo(domain)) 49 | ); 50 | 51 | const formattedResults = results.map((result, index) => ({ 52 | domain: domains[index], 53 | success: result.status === 'fulfilled' && result.value.success, 54 | data: result.status === 'fulfilled' ? result.value.data : null, 55 | error: result.status === 'rejected' ? result.reason.message : 56 | (result.value && !result.value.success ? result.value.message : null) 57 | })); 58 | 59 | res.json({ 60 | success: true, 61 | results: formattedResults 62 | }); 63 | } catch (error) { 64 | console.error('批量WHOIS查询错误:', error); 65 | res.status(500).json({ error: '批量WHOIS查询失败' }); 66 | } 67 | }); 68 | 69 | module.exports = router; 70 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import store from '@/store' 3 | 4 | const routes = [ 5 | { 6 | path: '/', 7 | name: 'Home', 8 | redirect: '/domains' 9 | }, 10 | { 11 | path: '/login', 12 | name: 'Login', 13 | component: () => import('@/views/Login.vue'), 14 | meta: { requiresGuest: true } 15 | }, 16 | { 17 | path: '/domains', 18 | name: 'Domains', 19 | component: () => import('@/views/Domains.vue'), 20 | meta: { requiresAuth: true } 21 | }, 22 | { 23 | path: '/admin', 24 | name: 'Admin', 25 | component: () => import('@/views/Admin.vue'), 26 | meta: { requiresAuth: true, requiresAdmin: true } 27 | }, 28 | { 29 | path: '/dashboard', 30 | name: 'Dashboard', 31 | component: () => import('@/views/Dashboard.vue'), 32 | meta: { requiresAuth: true, requiresAdmin: true } 33 | } 34 | ] 35 | 36 | const router = createRouter({ 37 | history: createWebHistory(process.env.BASE_URL), 38 | routes 39 | }) 40 | 41 | // 路由守卫 42 | router.beforeEach(async (to, from, next) => { 43 | const isAuthenticated = store.getters['auth/isAuthenticated'] 44 | const userRole = store.getters['auth/userRole'] 45 | 46 | // 如果需要登录但未登录,跳转到登录页 47 | if (to.matched.some(record => record.meta.requiresAuth)) { 48 | if (!isAuthenticated) { 49 | // 尝试从localStorage恢复token 50 | const token = localStorage.getItem('token') 51 | if (token) { 52 | try { 53 | await store.dispatch('auth/verifyToken', token) 54 | // 验证成功,继续导航 55 | if (to.matched.some(record => record.meta.requiresAdmin)) { 56 | if (userRole !== 'admin') { 57 | next('/domains') 58 | } else { 59 | next() 60 | } 61 | } else { 62 | next() 63 | } 64 | } catch (error) { 65 | // token无效,跳转到登录页 66 | next('/login') 67 | } 68 | } else { 69 | next('/login') 70 | } 71 | } else { 72 | // 已登录,检查管理员权限 73 | if (to.matched.some(record => record.meta.requiresAdmin)) { 74 | if (userRole !== 'admin') { 75 | next('/domains') 76 | } else { 77 | next() 78 | } 79 | } else { 80 | next() 81 | } 82 | } 83 | } else if (to.matched.some(record => record.meta.requiresGuest)) { 84 | // 如果已登录但访问guest页面,跳转到首页 85 | if (isAuthenticated) { 86 | next('/domains') 87 | } else { 88 | next() 89 | } 90 | } else { 91 | next() 92 | } 93 | }) 94 | 95 | export default router 96 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | // 格式化日期 4 | export const formatDate = (date, format = 'YYYY-MM-DD') => { 5 | if (!date) return '-' 6 | return dayjs(date).format(format) 7 | } 8 | 9 | // 计算剩余天数 10 | export const calculateDaysRemaining = (expirationDate) => { 11 | if (!expirationDate) return 'N/A' 12 | const now = dayjs() 13 | const expiry = dayjs(expirationDate) 14 | return expiry.diff(now, 'day') 15 | } 16 | 17 | // 获取状态颜色 18 | export const getStatusColor = (daysRemaining) => { 19 | if (daysRemaining === 'N/A' || daysRemaining < 0) return '#909399' 20 | if (daysRemaining <= 7) return '#f56c6c' 21 | if (daysRemaining <= 30) return '#e6a23c' 22 | if (daysRemaining <= 90) return '#f7ba2a' 23 | return '#67c23a' 24 | } 25 | 26 | // 获取状态文本 27 | export const getStatusText = (daysRemaining) => { 28 | if (daysRemaining === 'N/A') return '未知' 29 | if (daysRemaining < 0) return '已过期' 30 | if (daysRemaining <= 7) return '紧急' 31 | if (daysRemaining <= 30) return '警告' 32 | if (daysRemaining <= 90) return '注意' 33 | return '正常' 34 | } 35 | 36 | // 计算进度百分比 37 | export const calculateProgress = (registrationDate, expirationDate) => { 38 | if (!registrationDate || !expirationDate) return 0 39 | 40 | const start = dayjs(registrationDate) 41 | const end = dayjs(expirationDate) 42 | const now = dayjs() 43 | 44 | const total = end.diff(start, 'day') 45 | const elapsed = now.diff(start, 'day') 46 | 47 | if (total <= 0) return 0 48 | 49 | const progress = (elapsed / total) * 100 50 | return Math.min(100, Math.max(0, progress)) 51 | } 52 | 53 | // 域名分类 54 | export const categorizeDomains = (domains) => { 55 | const cfTopLevel = [] 56 | const cfSecondLevelAndCustom = [] 57 | 58 | domains.forEach(domain => { 59 | if (domain.system === 'Cloudflare' && domain.domain.split('.').length === 2) { 60 | cfTopLevel.push(domain) 61 | } else { 62 | cfSecondLevelAndCustom.push(domain) 63 | } 64 | }) 65 | 66 | return { 67 | cfTopLevel: cfTopLevel.sort((a, b) => a.domain.localeCompare(b.domain)), 68 | cfSecondLevelAndCustom: cfSecondLevelAndCustom.sort((a, b) => a.domain.localeCompare(b.domain)) 69 | } 70 | } 71 | 72 | // 验证域名格式 73 | export const validateDomain = (domain) => { 74 | const regex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ 75 | return regex.test(domain) 76 | } 77 | 78 | // 复制到剪贴板 79 | export const copyToClipboard = (text) => { 80 | if (navigator.clipboard) { 81 | return navigator.clipboard.writeText(text) 82 | } else { 83 | // 兜底方案 84 | const textArea = document.createElement('textarea') 85 | textArea.value = text 86 | document.body.appendChild(textArea) 87 | textArea.select() 88 | document.execCommand('copy') 89 | document.body.removeChild(textArea) 90 | return Promise.resolve() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /self-hosted/backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const helmet = require('helmet'); 4 | const compression = require('compression'); 5 | const rateLimit = require('express-rate-limit'); 6 | const dotenv = require('dotenv'); 7 | const winston = require('winston'); 8 | const path = require('path'); 9 | 10 | // 加载环境变量 11 | dotenv.config(); 12 | 13 | // 配置日志 14 | const logger = winston.createLogger({ 15 | level: process.env.LOG_LEVEL || 'info', 16 | format: winston.format.combine( 17 | winston.format.timestamp(), 18 | winston.format.errors({ stack: true }), 19 | winston.format.json() 20 | ), 21 | transports: [ 22 | new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), 23 | new winston.transports.File({ filename: 'logs/combined.log' }), 24 | new winston.transports.Console({ 25 | format: winston.format.simple() 26 | }) 27 | ] 28 | }); 29 | 30 | const app = express(); 31 | const PORT = process.env.PORT || 3001; 32 | 33 | // 中间件 34 | app.use(helmet()); 35 | app.use(compression()); 36 | app.use(cors({ 37 | origin: process.env.FRONTEND_URL || 'http://localhost:3000', 38 | credentials: true 39 | })); 40 | 41 | // 速率限制 42 | const limiter = rateLimit({ 43 | windowMs: 15 * 60 * 1000, // 15分钟 44 | max: 100, // 限制每个IP最多100个请求 45 | message: '请求过于频繁,请稍后再试' 46 | }); 47 | app.use('/api/', limiter); 48 | 49 | app.use(express.json()); 50 | app.use(express.urlencoded({ extended: true })); 51 | 52 | // 导入路由 53 | const authRoutes = require('./routes/auth'); 54 | const domainRoutes = require('./routes/domains'); 55 | const whoisRoutes = require('./routes/whois'); 56 | 57 | // 使用路由 58 | app.use('/api/auth', authRoutes); 59 | app.use('/api/domains', domainRoutes); 60 | app.use('/api/whois', whoisRoutes); 61 | 62 | // 健康检查 63 | app.get('/api/health', (req, res) => { 64 | res.json({ 65 | status: 'ok', 66 | timestamp: new Date().toISOString(), 67 | version: '2.0.0' 68 | }); 69 | }); 70 | 71 | // 错误处理中间件 72 | app.use((error, req, res, next) => { 73 | logger.error('Unhandled error:', error); 74 | res.status(error.status || 500).json({ 75 | error: '服务器内部错误', 76 | message: process.env.NODE_ENV === 'development' ? error.message : '请稍后重试' 77 | }); 78 | }); 79 | 80 | // 404处理 81 | app.use('*', (req, res) => { 82 | res.status(404).json({ error: '接口不存在' }); 83 | }); 84 | 85 | // 初始化数据库 86 | const db = require('./utils/database'); 87 | db.init().then(() => { 88 | logger.info('数据库初始化完成'); 89 | 90 | // 启动服务器 91 | app.listen(PORT, () => { 92 | logger.info(`DomainKeeper 后端服务已启动,端口:${PORT}`); 93 | logger.info(`环境:${process.env.NODE_ENV || 'development'}`); 94 | }); 95 | }).catch((error) => { 96 | logger.error('数据库初始化失败:', error); 97 | process.exit(1); 98 | }); 99 | 100 | // 启动定时同步任务 101 | const syncService = require('./services/syncService'); 102 | syncService.start(); 103 | 104 | module.exports = app; 105 | -------------------------------------------------------------------------------- /self-hosted/backend/services/whoisService.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | class WhoisService { 4 | constructor() { 5 | this.proxyUrl = process.env.WHOIS_PROXY_URL; 6 | } 7 | 8 | async fetchWhoisInfo(domain, includeRaw = false) { 9 | try { 10 | if (!this.proxyUrl) { 11 | return { 12 | success: false, 13 | message: 'WHOIS代理服务未配置' 14 | }; 15 | } 16 | 17 | console.log(`查询WHOIS信息: ${domain}`); 18 | 19 | const response = await axios.get(`${this.proxyUrl}/whois/${domain}`, { 20 | timeout: 30000 // 30秒超时 21 | }); 22 | 23 | const whoisData = response.data; 24 | 25 | if (whoisData.error) { 26 | return { 27 | success: false, 28 | message: whoisData.message || 'WHOIS查询失败' 29 | }; 30 | } 31 | 32 | const result = { 33 | success: true, 34 | data: { 35 | registrar: whoisData.registrar || 'Unknown', 36 | registrationDate: this.formatDate(whoisData.creationDate) || 'Unknown', 37 | expirationDate: this.formatDate(whoisData.expirationDate) || 'Unknown', 38 | nameservers: whoisData.nameServers || [], 39 | status: whoisData.status || [] 40 | } 41 | }; 42 | 43 | if (includeRaw) { 44 | result.rawData = whoisData.rawData || ''; 45 | } 46 | 47 | return result; 48 | } catch (error) { 49 | console.error(`WHOIS查询失败 (${domain}):`, error); 50 | 51 | let errorMessage = error.message; 52 | if (error.code === 'ECONNREFUSED') { 53 | errorMessage = 'WHOIS代理服务连接失败'; 54 | } else if (error.code === 'ETIMEDOUT') { 55 | errorMessage = 'WHOIS查询超时'; 56 | } else if (error.response) { 57 | errorMessage = `WHOIS服务返回错误: ${error.response.status}`; 58 | } 59 | 60 | return { 61 | success: false, 62 | message: errorMessage 63 | }; 64 | } 65 | } 66 | 67 | formatDate(dateString) { 68 | if (!dateString) return null; 69 | 70 | try { 71 | const date = new Date(dateString); 72 | if (isNaN(date.getTime())) { 73 | return dateString; // 如果无法解析,返回原始字符串 74 | } 75 | return date.toISOString().split('T')[0]; 76 | } catch (error) { 77 | return dateString; 78 | } 79 | } 80 | 81 | async batchFetchWhoisInfo(domains) { 82 | const results = []; 83 | 84 | for (const domain of domains) { 85 | try { 86 | const result = await this.fetchWhoisInfo(domain); 87 | results.push({ 88 | domain, 89 | ...result 90 | }); 91 | 92 | // 避免请求过于频繁,每次查询后等待1秒 93 | await new Promise(resolve => setTimeout(resolve, 1000)); 94 | } catch (error) { 95 | results.push({ 96 | domain, 97 | success: false, 98 | message: error.message 99 | }); 100 | } 101 | } 102 | 103 | return results; 104 | } 105 | } 106 | 107 | module.exports = new WhoisService(); 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Production builds 8 | /dist/ 9 | /build/ 10 | *.tgz 11 | *.tar.gz 12 | 13 | # Environment files 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | .env.production 20 | 21 | # Database files 22 | *.db 23 | *.sqlite 24 | *.sqlite3 25 | self-hosted/backend/data/*.db 26 | self-hosted/backend/data/*.sqlite* 27 | 28 | # Log files 29 | logs/ 30 | *.log 31 | self-hosted/backend/logs/ 32 | 33 | # IDE and editor files 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | *.swo 38 | *~ 39 | 40 | # OS generated files 41 | .DS_Store 42 | .DS_Store? 43 | ._* 44 | .Spotlight-V100 45 | .Trashes 46 | ehthumbs.db 47 | Thumbs.db 48 | 49 | # Python virtual environment 50 | .venv/ 51 | venv/ 52 | __pycache__/ 53 | *.pyc 54 | 55 | # Docker 56 | .dockerignore 57 | 58 | # Temporary files 59 | tmp/ 60 | temp/ 61 | *.tmp 62 | 63 | # Coverage reports 64 | coverage/ 65 | *.lcov 66 | 67 | # Optional npm cache directory 68 | .npm 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # ESLint cache 80 | .eslintcache 81 | 82 | # Parcel-bundler cache (https://parceljs.log/) 83 | .cache 84 | .parcel-cache 85 | 86 | # next.js build output 87 | .next 88 | 89 | # nuxt.js build output 90 | .nuxt 91 | 92 | # rollup.js default build output 93 | dist/ 94 | 95 | # Uncomment the public line in if your project uses Gatsby 96 | # https://nextjs.org/docs/api-routes/introduction 97 | # public 98 | 99 | # Storybook build outputs 100 | .out 101 | .storybook-out 102 | 103 | # Temporary folders 104 | tmp/ 105 | temp/ 106 | 107 | # Runtime data 108 | pids 109 | *.pid 110 | *.seed 111 | *.pid.lock 112 | 113 | # Directory for instrumented libs generated by jscoverage/JSCover 114 | lib-cov 115 | 116 | # Coverage directory used by tools like istanbul 117 | coverage 118 | 119 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 120 | .grunt 121 | 122 | # Bower dependency directory (https://bower.io/) 123 | bower_components 124 | 125 | # node-waf configuration 126 | .lock-wscript 127 | 128 | # Compiled binary addons (https://nodejs.org/api/addons.html) 129 | build/Release 130 | 131 | # Dependency directories 132 | jspm_packages/ 133 | 134 | # TypeScript cache 135 | *.tsbuildinfo 136 | 137 | # Optional npm cache directory 138 | .npm 139 | 140 | # Optional eslint cache 141 | .eslintcache 142 | 143 | # Output of 'npm pack' 144 | *.tgz 145 | 146 | # Yarn Integrity file 147 | .yarn-integrity 148 | 149 | # dotenv environment variables file 150 | .env.test 151 | 152 | # parcel-bundler cache (https://parceljs.org/) 153 | .cache 154 | .parcel-cache 155 | 156 | # Next.js build output 157 | .next 158 | 159 | # Nuxt.js build / generate output 160 | .nuxt 161 | dist 162 | 163 | # Storybook build outputs 164 | .out 165 | .storybook-out 166 | 167 | # Temporary folders 168 | tmp/ 169 | temp/ 170 | 171 | # Editor directories and files 172 | .vscode/* 173 | !.vscode/extensions.json 174 | .idea 175 | .DS_Store 176 | *.suo 177 | *.ntvs* 178 | *.njsproj 179 | *.sln 180 | *.sw? 181 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import api from '@/utils/api' 2 | 3 | const state = { 4 | token: localStorage.getItem('token') || null, 5 | user: JSON.parse(localStorage.getItem('user') || 'null'), 6 | isAuthenticated: false 7 | } 8 | 9 | const getters = { 10 | isAuthenticated: state => state.isAuthenticated, 11 | user: state => state.user, 12 | userRole: state => state.user?.role || state.user?.type || null, 13 | isAdmin: state => state.user?.role === 'admin' 14 | } 15 | 16 | const mutations = { 17 | SET_TOKEN(state, token) { 18 | state.token = token 19 | state.isAuthenticated = !!token 20 | if (token) { 21 | localStorage.setItem('token', token) 22 | } else { 23 | localStorage.removeItem('token') 24 | } 25 | }, 26 | 27 | SET_USER(state, user) { 28 | state.user = user 29 | if (user) { 30 | localStorage.setItem('user', JSON.stringify(user)) 31 | } else { 32 | localStorage.removeItem('user') 33 | } 34 | }, 35 | 36 | LOGOUT(state) { 37 | state.token = null 38 | state.user = null 39 | state.isAuthenticated = false 40 | localStorage.removeItem('token') 41 | localStorage.removeItem('user') 42 | } 43 | } 44 | 45 | const actions = { 46 | async login({ commit }, { username, password, type }) { 47 | try { 48 | const response = await api.post('/auth/login', { 49 | username, 50 | password, 51 | type 52 | }) 53 | 54 | const { token, user } = response.data 55 | 56 | commit('SET_TOKEN', token) 57 | commit('SET_USER', user) 58 | 59 | // 设置API默认token 60 | api.defaults.headers.common['Authorization'] = `Bearer ${token}` 61 | 62 | return { success: true } 63 | } catch (error) { 64 | return { 65 | success: false, 66 | message: error.response?.data?.error || '登录失败' 67 | } 68 | } 69 | }, 70 | 71 | async verifyToken({ commit }, token) { 72 | try { 73 | api.defaults.headers.common['Authorization'] = `Bearer ${token}` 74 | 75 | const response = await api.post('/auth/verify') 76 | const { user } = response.data 77 | 78 | commit('SET_TOKEN', token) 79 | commit('SET_USER', user) 80 | 81 | return true 82 | } catch (error) { 83 | commit('LOGOUT') 84 | delete api.defaults.headers.common['Authorization'] 85 | throw error 86 | } 87 | }, 88 | 89 | logout({ commit }) { 90 | commit('LOGOUT') 91 | delete api.defaults.headers.common['Authorization'] 92 | }, 93 | 94 | async changePassword(context, { oldPassword, newPassword }) { 95 | try { 96 | await api.post('/auth/change-password', { 97 | oldPassword, 98 | newPassword 99 | }) 100 | return { success: true } 101 | } catch (error) { 102 | return { 103 | success: false, 104 | message: error.response?.data?.error || '修改密码失败' 105 | } 106 | } 107 | } 108 | } 109 | 110 | // 初始化token 111 | if (state.token) { 112 | api.defaults.headers.common['Authorization'] = `Bearer ${state.token}` 113 | state.isAuthenticated = true 114 | } 115 | 116 | export default { 117 | namespaced: true, 118 | state, 119 | getters, 120 | mutations, 121 | actions 122 | } 123 | -------------------------------------------------------------------------------- /self-hosted/frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | error_log /var/log/nginx/error.log; 4 | pid /run/nginx.pid; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | http { 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | 15 | access_log /var/log/nginx/access.log main; 16 | 17 | sendfile on; 18 | tcp_nopush on; 19 | tcp_nodelay on; 20 | keepalive_timeout 65; 21 | types_hash_max_size 2048; 22 | 23 | include /etc/nginx/mime.types; 24 | default_type application/octet-stream; 25 | 26 | # Gzip 压缩 27 | gzip on; 28 | gzip_vary on; 29 | gzip_min_length 1024; 30 | gzip_proxied any; 31 | gzip_comp_level 6; 32 | gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; 33 | 34 | server { 35 | listen 80; 36 | server_name localhost; 37 | 38 | # Vue.js 单页应用配置 39 | location / { 40 | root /usr/share/nginx/html; 41 | index index.html index.htm; 42 | try_files $uri $uri/ /index.html; 43 | 44 | # 安全头 45 | add_header X-Frame-Options "SAMEORIGIN" always; 46 | add_header X-XSS-Protection "1; mode=block" always; 47 | add_header X-Content-Type-Options "nosniff" always; 48 | add_header Referrer-Policy "no-referrer-when-downgrade" always; 49 | add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; 50 | } 51 | 52 | # API 代理到后端 53 | location /api/ { 54 | proxy_pass http://backend:3001; 55 | proxy_http_version 1.1; 56 | proxy_set_header Upgrade $http_upgrade; 57 | proxy_set_header Connection 'upgrade'; 58 | proxy_set_header Host $host; 59 | proxy_set_header X-Real-IP $remote_addr; 60 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 61 | proxy_set_header X-Forwarded-Proto $scheme; 62 | proxy_cache_bypass $http_upgrade; 63 | 64 | # CORS 处理 65 | add_header 'Access-Control-Allow-Origin' '*'; 66 | add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; 67 | add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; 68 | 69 | if ($request_method = 'OPTIONS') { 70 | add_header 'Access-Control-Allow-Origin' '*'; 71 | add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; 72 | add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; 73 | add_header 'Access-Control-Max-Age' 1728000; 74 | add_header 'Content-Type' 'text/plain; charset=utf-8'; 75 | add_header 'Content-Length' 0; 76 | return 204; 77 | } 78 | } 79 | 80 | # 静态资源缓存 81 | location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { 82 | expires 1y; 83 | add_header Cache-Control "public, immutable"; 84 | } 85 | 86 | # 禁止访问隐藏文件 87 | location ~ /\. { 88 | deny all; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /self-hosted/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # DomainKeeper 一键部署脚本 4 | # 适用于 Ubuntu/Debian 系统 5 | 6 | set -e 7 | 8 | echo "================================" 9 | echo "DomainKeeper 自动部署脚本" 10 | echo "================================" 11 | 12 | # 检查是否为root用户 13 | if [ "$EUID" -ne 0 ]; then 14 | echo "请以root权限运行此脚本" 15 | exit 1 16 | fi 17 | 18 | # 更新系统 19 | echo "正在更新系统包..." 20 | apt-get update 21 | 22 | # 安装必要的软件 23 | echo "正在安装必要的软件..." 24 | apt-get install -y curl wget git 25 | 26 | # 安装Docker 27 | if ! command -v docker &> /dev/null; then 28 | echo "正在安装Docker..." 29 | curl -fsSL https://get.docker.com -o get-docker.sh 30 | sh get-docker.sh 31 | systemctl enable docker 32 | systemctl start docker 33 | rm get-docker.sh 34 | else 35 | echo "Docker已安装" 36 | fi 37 | 38 | # 安装Docker Compose 39 | if ! command -v docker-compose &> /dev/null; then 40 | echo "正在安装Docker Compose..." 41 | curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 42 | chmod +x /usr/local/bin/docker-compose 43 | else 44 | echo "Docker Compose已安装" 45 | fi 46 | 47 | # 创建项目目录 48 | PROJECT_DIR="/opt/domainkeeper" 49 | echo "正在创建项目目录: $PROJECT_DIR" 50 | mkdir -p $PROJECT_DIR 51 | cd $PROJECT_DIR 52 | 53 | # 下载项目文件(这里需要根据实际情况修改) 54 | echo "正在下载项目文件..." 55 | if [ ! -d ".git" ]; then 56 | # 如果不是git仓库,这里可以替换为实际的下载方式 57 | echo "请手动下载项目文件到 $PROJECT_DIR 目录" 58 | echo "或者修改此脚本中的下载逻辑" 59 | # git clone https://github.com/your-repo/domainkeeper.git . 60 | fi 61 | 62 | # 复制环境变量配置文件 63 | if [ ! -f ".env" ]; then 64 | echo "正在创建环境配置文件..." 65 | cp .env.example .env 66 | 67 | # 生成随机JWT密钥 68 | JWT_SECRET=$(openssl rand -base64 32) 69 | sed -i "s/your_super_secret_jwt_key_change_this_in_production/$JWT_SECRET/" .env 70 | 71 | echo "=======================================" 72 | echo "重要:请编辑 .env 文件配置您的设置:" 73 | echo "- 修改管理员密码 (ADMIN_PASSWORD)" 74 | echo "- 配置Cloudflare API Token (可选)" 75 | echo "- 配置WHOIS服务地址 (可选)" 76 | echo "=======================================" 77 | echo "配置文件位置: $PROJECT_DIR/.env" 78 | fi 79 | 80 | # 设置文件权限 81 | chown -R root:root $PROJECT_DIR 82 | chmod -R 755 $PROJECT_DIR 83 | 84 | # 创建systemd服务文件 85 | echo "正在创建系统服务..." 86 | cat > /etc/systemd/system/domainkeeper.service << EOF 87 | [Unit] 88 | Description=DomainKeeper Service 89 | Requires=docker.service 90 | After=docker.service 91 | 92 | [Service] 93 | Type=oneshot 94 | RemainAfterExit=yes 95 | WorkingDirectory=$PROJECT_DIR 96 | ExecStart=/usr/local/bin/docker-compose up -d 97 | ExecStop=/usr/local/bin/docker-compose down 98 | TimeoutStartSec=0 99 | 100 | [Install] 101 | WantedBy=multi-user.target 102 | EOF 103 | 104 | # 启用并启动服务 105 | systemctl daemon-reload 106 | systemctl enable domainkeeper.service 107 | 108 | # 设置防火墙(如果ufw存在) 109 | if command -v ufw &> /dev/null; then 110 | echo "正在配置防火墙..." 111 | ufw allow 80/tcp 112 | ufw allow 3001/tcp 113 | fi 114 | 115 | echo "=======================================" 116 | echo "部署完成!" 117 | echo "=======================================" 118 | echo "配置文件: $PROJECT_DIR/.env" 119 | echo "启动服务: systemctl start domainkeeper" 120 | echo "查看状态: systemctl status domainkeeper" 121 | echo "查看日志: cd $PROJECT_DIR && docker-compose logs -f" 122 | echo "" 123 | echo "请先编辑配置文件,然后启动服务:" 124 | echo "1. nano $PROJECT_DIR/.env" 125 | echo "2. systemctl start domainkeeper" 126 | echo "" 127 | echo "访问地址: http://your-server-ip" 128 | echo "=======================================" 129 | -------------------------------------------------------------------------------- /self-hosted/backend/utils/database.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const dbPath = process.env.DATABASE_PATH || './data/domains.db'; 6 | const dbDir = path.dirname(dbPath); 7 | 8 | // 确保数据目录存在 9 | if (!fs.existsSync(dbDir)) { 10 | fs.mkdirSync(dbDir, { recursive: true }); 11 | } 12 | 13 | let db; 14 | 15 | const init = () => { 16 | return new Promise((resolve, reject) => { 17 | db = new sqlite3.Database(dbPath, (err) => { 18 | if (err) { 19 | reject(err); 20 | return; 21 | } 22 | 23 | // 创建表 24 | const createTables = ` 25 | CREATE TABLE IF NOT EXISTS users ( 26 | id INTEGER PRIMARY KEY AUTOINCREMENT, 27 | username TEXT UNIQUE NOT NULL, 28 | password TEXT NOT NULL, 29 | role TEXT DEFAULT 'admin', 30 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP 31 | ); 32 | 33 | CREATE TABLE IF NOT EXISTS domains ( 34 | id INTEGER PRIMARY KEY AUTOINCREMENT, 35 | domain TEXT UNIQUE NOT NULL, 36 | system TEXT, 37 | registrar TEXT, 38 | registration_date TEXT, 39 | expiration_date TEXT, 40 | zone_id TEXT, 41 | is_custom BOOLEAN DEFAULT 0, 42 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 43 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 44 | ); 45 | 46 | CREATE TABLE IF NOT EXISTS whois_cache ( 47 | id INTEGER PRIMARY KEY AUTOINCREMENT, 48 | domain TEXT UNIQUE NOT NULL, 49 | whois_data TEXT, 50 | cached_at DATETIME DEFAULT CURRENT_TIMESTAMP, 51 | expires_at DATETIME 52 | ); 53 | 54 | CREATE TABLE IF NOT EXISTS sync_logs ( 55 | id INTEGER PRIMARY KEY AUTOINCREMENT, 56 | sync_type TEXT NOT NULL, 57 | status TEXT NOT NULL, 58 | message TEXT, 59 | domains_count INTEGER, 60 | sync_at DATETIME DEFAULT CURRENT_TIMESTAMP 61 | ); 62 | `; 63 | 64 | db.exec(createTables, (err) => { 65 | if (err) { 66 | reject(err); 67 | } else { 68 | resolve(); 69 | } 70 | }); 71 | }); 72 | }); 73 | }; 74 | 75 | const getDb = () => { 76 | if (!db) { 77 | throw new Error('数据库未初始化'); 78 | } 79 | return db; 80 | }; 81 | 82 | const close = () => { 83 | return new Promise((resolve, reject) => { 84 | if (db) { 85 | db.close((err) => { 86 | if (err) { 87 | reject(err); 88 | } else { 89 | resolve(); 90 | } 91 | }); 92 | } else { 93 | resolve(); 94 | } 95 | }); 96 | }; 97 | 98 | // 辅助函数 99 | const run = (sql, params = []) => { 100 | return new Promise((resolve, reject) => { 101 | getDb().run(sql, params, function(err) { 102 | if (err) { 103 | reject(err); 104 | } else { 105 | resolve({ id: this.lastID, changes: this.changes }); 106 | } 107 | }); 108 | }); 109 | }; 110 | 111 | const get = (sql, params = []) => { 112 | return new Promise((resolve, reject) => { 113 | getDb().get(sql, params, (err, row) => { 114 | if (err) { 115 | reject(err); 116 | } else { 117 | resolve(row); 118 | } 119 | }); 120 | }); 121 | }; 122 | 123 | const all = (sql, params = []) => { 124 | return new Promise((resolve, reject) => { 125 | getDb().all(sql, params, (err, rows) => { 126 | if (err) { 127 | reject(err); 128 | } else { 129 | resolve(rows); 130 | } 131 | }); 132 | }); 133 | }; 134 | 135 | module.exports = { 136 | init, 137 | getDb, 138 | close, 139 | run, 140 | get, 141 | all 142 | }; 143 | -------------------------------------------------------------------------------- /self-hosted/backend/services/cloudflareService.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const db = require('../utils/database'); 3 | 4 | class CloudflareService { 5 | constructor() { 6 | this.apiToken = process.env.CF_API_TOKEN; 7 | this.baseURL = 'https://api.cloudflare.com/client/v4'; 8 | } 9 | 10 | async syncDomains() { 11 | try { 12 | if (!this.apiToken) { 13 | return { 14 | success: false, 15 | message: 'Cloudflare API Token未配置' 16 | }; 17 | } 18 | 19 | console.log('开始同步Cloudflare域名...'); 20 | 21 | const zones = await this.fetchAllZones(); 22 | console.log(`从Cloudflare获取到 ${zones.length} 个域名`); 23 | 24 | let addedCount = 0; 25 | let updatedCount = 0; 26 | const domainNames = []; 27 | 28 | for (const zone of zones) { 29 | domainNames.push(zone.name); 30 | 31 | // 检查域名是否已存在 32 | const existing = await db.get('SELECT * FROM domains WHERE domain = ?', [zone.name]); 33 | 34 | if (existing) { 35 | // 更新已存在的域名信息(但不覆盖自定义域名的信息) 36 | if (!existing.is_custom) { 37 | await db.run( 38 | 'UPDATE domains SET system = ?, zone_id = ?, updated_at = CURRENT_TIMESTAMP WHERE domain = ?', 39 | ['Cloudflare', zone.id, zone.name] 40 | ); 41 | updatedCount++; 42 | } 43 | } else { 44 | // 添加新域名 45 | await db.run( 46 | 'INSERT INTO domains (domain, system, zone_id, registration_date, is_custom) VALUES (?, ?, ?, ?, ?)', 47 | [zone.name, 'Cloudflare', zone.id, zone.created_on.split('T')[0], 0] 48 | ); 49 | addedCount++; 50 | } 51 | } 52 | 53 | // 清理不再存在于Cloudflare的域名(但保留自定义域名) 54 | const cfDomainsList = zones.map(z => z.name); 55 | const existingCfDomains = await db.all( 56 | 'SELECT domain FROM domains WHERE system = ? AND is_custom = 0', 57 | ['Cloudflare'] 58 | ); 59 | 60 | let removedCount = 0; 61 | for (const existingDomain of existingCfDomains) { 62 | if (!cfDomainsList.includes(existingDomain.domain)) { 63 | await db.run('DELETE FROM domains WHERE domain = ? AND is_custom = 0', [existingDomain.domain]); 64 | removedCount++; 65 | } 66 | } 67 | 68 | console.log(`同步完成: 新增 ${addedCount} 个,更新 ${updatedCount} 个,移除 ${removedCount} 个`); 69 | 70 | return { 71 | success: true, 72 | message: `同步成功: 新增 ${addedCount} 个,更新 ${updatedCount} 个,移除 ${removedCount} 个`, 73 | count: zones.length, 74 | domains: domainNames, 75 | stats: { 76 | added: addedCount, 77 | updated: updatedCount, 78 | removed: removedCount 79 | } 80 | }; 81 | } catch (error) { 82 | console.error('Cloudflare同步失败:', error); 83 | return { 84 | success: false, 85 | message: error.message 86 | }; 87 | } 88 | } 89 | 90 | async fetchAllZones() { 91 | const zones = []; 92 | let page = 1; 93 | let totalPages = 1; 94 | 95 | do { 96 | const response = await axios.get(`${this.baseURL}/zones`, { 97 | headers: { 98 | 'Authorization': `Bearer ${this.apiToken}`, 99 | 'Content-Type': 'application/json' 100 | }, 101 | params: { 102 | page, 103 | per_page: 50 104 | } 105 | }); 106 | 107 | if (!response.data.success) { 108 | throw new Error('Cloudflare API请求失败: ' + JSON.stringify(response.data.errors)); 109 | } 110 | 111 | zones.push(...response.data.result); 112 | totalPages = response.data.result_info.total_pages; 113 | page++; 114 | } while (page <= totalPages); 115 | 116 | return zones; 117 | } 118 | 119 | async getZoneInfo(zoneId) { 120 | try { 121 | const response = await axios.get(`${this.baseURL}/zones/${zoneId}`, { 122 | headers: { 123 | 'Authorization': `Bearer ${this.apiToken}`, 124 | 'Content-Type': 'application/json' 125 | } 126 | }); 127 | 128 | if (!response.data.success) { 129 | throw new Error('获取Zone信息失败'); 130 | } 131 | 132 | return response.data.result; 133 | } catch (error) { 134 | console.error('获取Zone信息失败:', error); 135 | throw error; 136 | } 137 | } 138 | } 139 | 140 | module.exports = new CloudflareService(); 141 | -------------------------------------------------------------------------------- /self-hosted/backend/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bcrypt = require('bcryptjs'); 3 | const jwt = require('jsonwebtoken'); 4 | const db = require('../utils/database'); 5 | 6 | const router = express.Router(); 7 | 8 | // 初始化管理员用户 9 | const initAdmin = async () => { 10 | const adminUsername = process.env.ADMIN_USERNAME || 'admin'; 11 | const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'; 12 | 13 | try { 14 | const existingUser = await db.get('SELECT * FROM users WHERE username = ?', [adminUsername]); 15 | 16 | if (!existingUser) { 17 | const hashedPassword = await bcrypt.hash(adminPassword, 10); 18 | await db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', 19 | [adminUsername, hashedPassword, 'admin']); 20 | console.log(`管理员用户已创建: ${adminUsername}`); 21 | } 22 | } catch (error) { 23 | console.error('初始化管理员用户失败:', error); 24 | } 25 | }; 26 | 27 | // 延迟初始化管理员 28 | setTimeout(initAdmin, 1000); 29 | 30 | // 登录 31 | router.post('/login', async (req, res) => { 32 | try { 33 | const { username, password, type } = req.body; 34 | 35 | // 前台访问检查 36 | if (type === 'frontend') { 37 | const accessPassword = process.env.ACCESS_PASSWORD; 38 | if (!accessPassword) { 39 | // 如果没有设置前台密码,直接返回成功 40 | return res.json({ 41 | success: true, 42 | token: jwt.sign({ type: 'frontend' }, process.env.JWT_SECRET, { expiresIn: '24h' }), 43 | user: { type: 'frontend' } 44 | }); 45 | } 46 | 47 | if (password !== accessPassword) { 48 | return res.status(401).json({ error: '密码错误' }); 49 | } 50 | 51 | const token = jwt.sign({ type: 'frontend' }, process.env.JWT_SECRET, { expiresIn: '24h' }); 52 | return res.json({ 53 | success: true, 54 | token, 55 | user: { type: 'frontend' } 56 | }); 57 | } 58 | 59 | // 后台管理员登录 60 | if (!username || !password) { 61 | return res.status(400).json({ error: '用户名和密码不能为空' }); 62 | } 63 | 64 | const user = await db.get('SELECT * FROM users WHERE username = ?', [username]); 65 | if (!user) { 66 | return res.status(401).json({ error: '用户名或密码错误' }); 67 | } 68 | 69 | const isValidPassword = await bcrypt.compare(password, user.password); 70 | if (!isValidPassword) { 71 | return res.status(401).json({ error: '用户名或密码错误' }); 72 | } 73 | 74 | const token = jwt.sign( 75 | { id: user.id, username: user.username, role: user.role }, 76 | process.env.JWT_SECRET, 77 | { expiresIn: '24h' } 78 | ); 79 | 80 | res.json({ 81 | success: true, 82 | token, 83 | user: { 84 | id: user.id, 85 | username: user.username, 86 | role: user.role 87 | } 88 | }); 89 | } catch (error) { 90 | console.error('登录错误:', error); 91 | res.status(500).json({ error: '登录失败' }); 92 | } 93 | }); 94 | 95 | // 验证token 96 | router.post('/verify', (req, res) => { 97 | const token = req.headers.authorization?.replace('Bearer ', ''); 98 | 99 | if (!token) { 100 | return res.status(401).json({ error: '未提供token' }); 101 | } 102 | 103 | try { 104 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 105 | res.json({ success: true, user: decoded }); 106 | } catch (error) { 107 | res.status(401).json({ error: 'token无效或已过期' }); 108 | } 109 | }); 110 | 111 | // 修改密码 112 | router.post('/change-password', async (req, res) => { 113 | const token = req.headers.authorization?.replace('Bearer ', ''); 114 | 115 | if (!token) { 116 | return res.status(401).json({ error: '未提供token' }); 117 | } 118 | 119 | try { 120 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 121 | const { oldPassword, newPassword } = req.body; 122 | 123 | if (!oldPassword || !newPassword) { 124 | return res.status(400).json({ error: '旧密码和新密码不能为空' }); 125 | } 126 | 127 | const user = await db.get('SELECT * FROM users WHERE id = ?', [decoded.id]); 128 | if (!user) { 129 | return res.status(404).json({ error: '用户不存在' }); 130 | } 131 | 132 | const isValidPassword = await bcrypt.compare(oldPassword, user.password); 133 | if (!isValidPassword) { 134 | return res.status(401).json({ error: '原密码错误' }); 135 | } 136 | 137 | const hashedNewPassword = await bcrypt.hash(newPassword, 10); 138 | await db.run('UPDATE users SET password = ? WHERE id = ?', [hashedNewPassword, decoded.id]); 139 | 140 | res.json({ success: true, message: '密码修改成功' }); 141 | } catch (error) { 142 | console.error('修改密码错误:', error); 143 | res.status(500).json({ error: '修改密码失败' }); 144 | } 145 | }); 146 | 147 | module.exports = router; 148 | -------------------------------------------------------------------------------- /self-hosted/backend/services/syncService.js: -------------------------------------------------------------------------------- 1 | const cron = require('node-cron'); 2 | const cloudflareService = require('./cloudflareService'); 3 | const whoisService = require('./whoisService'); 4 | const db = require('../utils/database'); 5 | 6 | class SyncService { 7 | constructor() { 8 | this.isRunning = false; 9 | this.syncInterval = process.env.SYNC_INTERVAL || 60; // 默认60分钟 10 | } 11 | 12 | start() { 13 | // 每小时同步一次Cloudflare域名 14 | const cronExpression = `0 */${this.syncInterval} * * *`; 15 | 16 | console.log(`启动自动同步服务,间隔:${this.syncInterval}分钟`); 17 | 18 | cron.schedule(cronExpression, () => { 19 | this.runSync(); 20 | }); 21 | 22 | // 服务启动后立即执行一次同步 23 | setTimeout(() => { 24 | this.runSync(); 25 | }, 5000); // 延迟5秒启动 26 | } 27 | 28 | async runSync() { 29 | if (this.isRunning) { 30 | console.log('同步任务已在运行中,跳过本次同步'); 31 | return; 32 | } 33 | 34 | this.isRunning = true; 35 | 36 | try { 37 | console.log('开始自动同步任务...'); 38 | 39 | // 1. 同步Cloudflare域名 40 | const cfSyncResult = await cloudflareService.syncDomains(); 41 | 42 | // 记录同步日志 43 | await db.run( 44 | 'INSERT INTO sync_logs (sync_type, status, message, domains_count) VALUES (?, ?, ?, ?)', 45 | ['auto_cloudflare', cfSyncResult.success ? 'success' : 'failed', cfSyncResult.message, cfSyncResult.count || 0] 46 | ); 47 | 48 | if (cfSyncResult.success) { 49 | console.log(`Cloudflare域名同步成功: ${cfSyncResult.message}`); 50 | 51 | // 2. 自动更新顶级域名的WHOIS信息(仅限没有缓存或缓存过期的) 52 | await this.updateExpiredWhoisCache(); 53 | } else { 54 | console.error(`Cloudflare域名同步失败: ${cfSyncResult.message}`); 55 | } 56 | 57 | } catch (error) { 58 | console.error('自动同步任务失败:', error); 59 | 60 | // 记录错误日志 61 | await db.run( 62 | 'INSERT INTO sync_logs (sync_type, status, message) VALUES (?, ?, ?)', 63 | ['auto_sync', 'error', error.message] 64 | ); 65 | } finally { 66 | this.isRunning = false; 67 | } 68 | } 69 | 70 | async updateExpiredWhoisCache() { 71 | try { 72 | // 获取需要更新WHOIS信息的域名(顶级域名且缓存过期或无缓存) 73 | const domainsNeedUpdate = await db.all(` 74 | SELECT d.domain 75 | FROM domains d 76 | LEFT JOIN whois_cache w ON d.domain = w.domain 77 | WHERE d.system = 'Cloudflare' 78 | AND LENGTH(d.domain) - LENGTH(REPLACE(d.domain, '.', '')) = 1 79 | AND (w.expires_at IS NULL OR w.expires_at < datetime('now')) 80 | LIMIT 5 81 | `); 82 | 83 | if (domainsNeedUpdate.length === 0) { 84 | console.log('没有需要更新WHOIS缓存的域名'); 85 | return; 86 | } 87 | 88 | console.log(`开始更新 ${domainsNeedUpdate.length} 个域名的WHOIS信息`); 89 | 90 | for (const domainRow of domainsNeedUpdate) { 91 | try { 92 | const whoisResult = await whoisService.fetchWhoisInfo(domainRow.domain); 93 | 94 | if (whoisResult.success) { 95 | // 更新域名信息 96 | await db.run( 97 | 'UPDATE domains SET registrar = ?, registration_date = ?, expiration_date = ?, updated_at = CURRENT_TIMESTAMP WHERE domain = ?', 98 | [whoisResult.data.registrar, whoisResult.data.registrationDate, whoisResult.data.expirationDate, domainRow.domain] 99 | ); 100 | 101 | // 更新WHOIS缓存 102 | const expiresAt = new Date(); 103 | expiresAt.setDate(expiresAt.getDate() + 7); // 7天后过期 104 | 105 | await db.run( 106 | 'INSERT OR REPLACE INTO whois_cache (domain, whois_data, expires_at) VALUES (?, ?, ?)', 107 | [domainRow.domain, JSON.stringify(whoisResult.data), expiresAt.toISOString()] 108 | ); 109 | 110 | console.log(`已更新域名 ${domainRow.domain} 的WHOIS信息`); 111 | } else { 112 | console.log(`域名 ${domainRow.domain} WHOIS查询失败: ${whoisResult.message}`); 113 | } 114 | 115 | // 避免请求过于频繁 116 | await new Promise(resolve => setTimeout(resolve, 2000)); 117 | } catch (error) { 118 | console.error(`更新域名 ${domainRow.domain} WHOIS信息失败:`, error); 119 | } 120 | } 121 | 122 | console.log('WHOIS信息更新完成'); 123 | } catch (error) { 124 | console.error('更新WHOIS缓存失败:', error); 125 | } 126 | } 127 | 128 | async manualSync() { 129 | return this.runSync(); 130 | } 131 | 132 | getStatus() { 133 | return { 134 | isRunning: this.isRunning, 135 | syncInterval: this.syncInterval, 136 | nextSync: this.getNextSyncTime() 137 | }; 138 | } 139 | 140 | getNextSyncTime() { 141 | const now = new Date(); 142 | const nextSync = new Date(now); 143 | nextSync.setMinutes(Math.ceil(now.getMinutes() / this.syncInterval) * this.syncInterval); 144 | nextSync.setSeconds(0); 145 | nextSync.setMilliseconds(0); 146 | 147 | return nextSync.toISOString(); 148 | } 149 | } 150 | 151 | module.exports = new SyncService(); 152 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // 自定义标题 2 | const CUSTOM_TITLE = "我的域名管理"; 3 | 4 | // 域名信息 5 | const DOMAINS = [ 6 | { domain: "example.com", registrationDate: "2022-01-01", expirationDate: "2027-01-01", system: "Cloudflare" }, 7 | { domain: "example.org", registrationDate: "2021-06-15", expirationDate: "2026-06-15", system: "GoDaddy" }, 8 | { domain: "example.net", registrationDate: "2021-06-15", expirationDate: "2024-06-15", system: "GoDaddy" }, 9 | // 在这里添加更多域名 10 | ]; 11 | 12 | addEventListener('fetch', event => { 13 | event.respondWith(handleRequest(event.request)) 14 | }); 15 | 16 | async function handleRequest(request) { 17 | return new Response(generateHTML(), { 18 | headers: { 'Content-Type': 'text/html' }, 19 | }); 20 | } 21 | 22 | function generateHTML() { 23 | const rows = DOMAINS.map(info => { 24 | const registrationDate = new Date(info.registrationDate); 25 | const expirationDate = new Date(info.expirationDate); 26 | const today = new Date(); 27 | const totalDays = (expirationDate - registrationDate) / (1000 * 60 * 60 * 24); 28 | const daysElapsed = (today - registrationDate) / (1000 * 60 * 60 * 24); 29 | const progressPercentage = Math.min(100, Math.max(0, (daysElapsed / totalDays) * 100)); 30 | const daysRemaining = Math.ceil((expirationDate - today) / (1000 * 60 * 60 * 24)); 31 | 32 | // 判断域名是否过期 33 | const isExpired = today > expirationDate; 34 | const statusColor = isExpired ? '#e74c3c' : '#2ecc71'; 35 | const statusText = isExpired ? '已过期' : '正常'; 36 | 37 | return ` 38 | 39 | 40 | ${info.domain} 41 | ${info.system} 42 | ${info.registrationDate} 43 | ${info.expirationDate} 44 | ${isExpired ? '已过期' : daysRemaining + ' 天'} 45 | 46 |
47 |
48 |
49 | 50 | 51 | `; 52 | }).join(''); 53 | 54 | return ` 55 | 56 | 57 | 58 | 59 | 60 | ${CUSTOM_TITLE} 61 | 133 | 134 | 135 |
136 |

${CUSTOM_TITLE}

137 |
138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | ${rows} 152 | 153 |
状态域名域名注册商注册时间过期时间剩余天数使用进度
154 |
155 |
156 | 159 | 160 | 161 | `; 162 | } 163 | -------------------------------------------------------------------------------- /self-hosted/README-zh.md: -------------------------------------------------------------------------------- 1 | # DomainKeeper 前后端分离版本 2 | 3 | ![DomainKeeper](https://img.shields.io/badge/DomainKeeper-v2.0.0-blue) 4 | ![Node.js](https://img.shields.io/badge/Node.js-18+-green) 5 | ![Vue.js](https://img.shields.io/badge/Vue.js-3.3+-green) 6 | ![Docker](https://img.shields.io/badge/Docker-supported-blue) 7 | ![License](https://img.shields.io/badge/License-MIT-yellow) 8 | 9 | 一个现代化的域名管理系统,支持前后端分离部署,提供直观的Web界面来管理和监控您的域名。 10 | 11 | ## ✨ 功能特性 12 | 13 | - 🌐 **域名管理**: 支持添加、编辑、删除域名记录 14 | - 🔄 **Cloudflare集成**: 自动同步Cloudflare域名信息 15 | - 🔍 **WHOIS查询**: 实时获取域名注册信息 16 | - 📊 **可视化仪表板**: 域名状态统计图表和过期监控 17 | - ⏰ **智能提醒**: 域名到期状态实时监控 18 | - 🔐 **权限分离**: 前台访问和后台管理独立控制 19 | - 🐳 **容器化部署**: Docker支持,一键部署 20 | - 📱 **响应式设计**: 完美适配桌面和移动设备 21 | - 🔧 **API支持**: RESTful API接口,支持扩展开发 22 | 23 | ## 🏗️ 技术架构 24 | 25 | ### 后端技术栈 26 | - **框架**: Node.js + Express.js 27 | - **数据库**: SQLite(轻量级,免维护) 28 | - **认证**: JWT Token 29 | - **任务调度**: node-cron 30 | - **API**: RESTful架构 31 | 32 | ### 前端技术栈 33 | - **框架**: Vue.js 3 + Composition API 34 | - **UI组件**: Element Plus 35 | - **状态管理**: Vuex 36 | - **路由**: Vue Router 37 | - **图表**: ECharts 38 | - **构建工具**: Vue CLI 39 | 40 | ## 🚀 快速开始 41 | 42 | ### 使用 Docker Compose(推荐) 43 | 44 | 1. **下载项目** 45 | ```bash 46 | git clone https://github.com/your-repo/domainkeeper.git 47 | cd domainkeeper/self-hosted 48 | ``` 49 | 50 | 2. **配置环境变量** 51 | ```bash 52 | cp .env.example .env 53 | nano .env 54 | ``` 55 | 56 | 3. **一键启动** 57 | ```bash 58 | docker-compose up -d 59 | ``` 60 | 61 | 4. **访问系统** 62 | - 前台: http://your-server-ip 63 | - 管理后台: 点击页面上的管理后台链接 64 | 65 | ### 自动部署脚本 66 | 67 | ```bash 68 | # Ubuntu/Debian 系统 69 | wget -O deploy.sh https://raw.githubusercontent.com/your-repo/domainkeeper/main/self-hosted/deploy.sh 70 | chmod +x deploy.sh 71 | sudo ./deploy.sh 72 | ``` 73 | 74 | ## ⚙️ 配置说明 75 | 76 | ### 必需配置 77 | 78 | ```env 79 | # JWT加密密钥(必须修改) 80 | JWT_SECRET=your_super_secret_jwt_key 81 | 82 | # 管理员账户 83 | ADMIN_USERNAME=admin 84 | ADMIN_PASSWORD=your_secure_password 85 | ``` 86 | 87 | ### 可选配置 88 | 89 | ```env 90 | # 前台访问密码(可选) 91 | ACCESS_PASSWORD= 92 | 93 | # Cloudflare API Token(用于自动同步) 94 | CF_API_TOKEN=your_cloudflare_api_token 95 | 96 | # WHOIS代理服务(用于域名信息查询) 97 | WHOIS_PROXY_URL=https://whois.0o11.com 98 | 99 | # 自定义设置 100 | CUSTOM_TITLE=我的域名管理 101 | SYNC_INTERVAL=60 102 | ``` 103 | 104 | ## 📸 功能截图 105 | 106 | ### 域名列表界面 107 | - 清晰展示所有域名状态 108 | - 过期时间倒计时 109 | - 使用进度可视化 110 | - 响应式表格设计 111 | 112 | ### 管理后台 113 | - 域名CRUD操作 114 | - Cloudflare自动同步 115 | - 批量管理功能 116 | - 系统统计信息 117 | 118 | ### 可视化仪表板 119 | - 域名状态分布图 120 | - 过期时间分析 121 | - 即将过期列表 122 | - 实时数据更新 123 | 124 | ## 🔧 API 接口 125 | 126 | 系统提供完整的RESTful API: 127 | 128 | ```http 129 | GET /api/domains # 获取域名列表 130 | POST /api/domains # 添加域名 131 | PUT /api/domains/:id # 更新域名 132 | DELETE /api/domains/:id # 删除域名 133 | POST /api/domains/sync-cloudflare # 同步Cloudflare 134 | GET /api/whois/:domain # 查询WHOIS信息 135 | POST /api/auth/login # 用户登录 136 | GET /api/domains/stats/overview # 获取统计信息 137 | ``` 138 | 139 | ## 🔒 安全特性 140 | 141 | - **JWT认证**: 安全的token机制 142 | - **权限分离**: 前台/后台独立权限 143 | - **密码加密**: bcrypt加密存储 144 | - **CORS保护**: 跨域请求保护 145 | - **速率限制**: API调用频次限制 146 | - **输入验证**: 严格的数据验证 147 | 148 | ## 🛠️ 开发指南 149 | 150 | ### 本地开发环境 151 | 152 | ```bash 153 | # 后端开发 154 | cd backend 155 | npm install 156 | npm run dev 157 | 158 | # 前端开发 159 | cd frontend 160 | npm install 161 | npm run serve 162 | ``` 163 | 164 | ### 项目结构 165 | 166 | ``` 167 | self-hosted/ 168 | ├── backend/ # 后端代码 169 | │ ├── routes/ # API路由 170 | │ ├── services/ # 业务逻辑 171 | │ ├── middleware/ # 中间件 172 | │ └── utils/ # 工具类 173 | ├── frontend/ # 前端代码 174 | │ ├── src/ 175 | │ │ ├── views/ # 页面组件 176 | │ │ ├── components/ # 通用组件 177 | │ │ ├── store/ # 状态管理 178 | │ │ └── utils/ # 工具函数 179 | │ └── public/ # 静态资源 180 | ├── docker-compose.yml # Docker编排 181 | └── README.md # 说明文档 182 | ``` 183 | 184 | ## 📊 系统要求 185 | 186 | ### 最低配置 187 | - **CPU**: 1核 188 | - **内存**: 512MB 189 | - **存储**: 1GB 190 | - **操作系统**: Linux/Windows/macOS 191 | 192 | ### 推荐配置 193 | - **CPU**: 2核 194 | - **内存**: 1GB 195 | - **存储**: 5GB 196 | - **操作系统**: Ubuntu 20.04+ 197 | 198 | ## 🔄 更新升级 199 | 200 | ```bash 201 | # 停止服务 202 | docker-compose down 203 | 204 | # 拉取更新 205 | git pull 206 | 207 | # 重新构建 208 | docker-compose up -d --build 209 | ``` 210 | 211 | ## 📝 许可证 212 | 213 | 本项目基于 MIT 许可证开源 - 查看 [LICENSE](LICENSE) 文件了解详情。 214 | 215 | ## 🤝 贡献指南 216 | 217 | 欢迎贡献代码!请遵循以下步骤: 218 | 219 | 1. Fork 本项目 220 | 2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) 221 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 222 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 223 | 5. 提交 Pull Request 224 | 225 | ## 📞 支持与反馈 226 | 227 | - **GitHub Issues**: 报告Bug或功能请求 228 | - **文档**: 查看完整部署文档 229 | - **社区**: 加入讨论群组 230 | 231 | ## 🌟 Star History 232 | 233 | [![Star History Chart](https://api.star-history.com/svg?repos=ypq123456789/domainkeeper&type=Date)](https://star-history.com/#ypq123456789/domainkeeper&Date) 234 | 235 | ## 🙏 致谢 236 | 237 | 感谢所有贡献者和使用者的支持! 238 | 239 | --- 240 | 241 | **开始使用 DomainKeeper,让域名管理变得简单高效!** 🚀 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目简介 2 | 3 | 这是一个简洁高效的域名可视化展示面板。提供了直观的界面,让用户能够一目了然地查看他们的域名组合,包括各个域名的状态、注册商、注册日期、过期日期和使用进度。 4 | 5 | ## 🚀 部署方式选择 6 | 7 | 本项目提供**三种部署方案**,请根据您的需求选择: 8 | 9 | ### 方案一:Cloudflare Workers 部署(推荐新手) 10 | - ✅ **部署简单**:无需服务器,几分钟完成部署 11 | - ✅ **免费使用**:利用 Cloudflare 免费额度 12 | - ✅ **稳定可靠**:基于 Cloudflare 全球网络 13 | - ❌ **功能受限**:受 Workers 环境限制 14 | - 📍 **适合场景**:个人使用、快速上线、不需要复杂功能 15 | 16 | ### 方案二:前后端分离自托管(推荐进阶) 17 | - ✅ **功能完整**:现代化 Web 应用,功能丰富 18 | - ✅ **完全控制**:可自定义扩展,数据完全可控 19 | - ✅ **现代技术**:Vue.js + Node.js 架构 20 | - ❌ **需要服务器**:需要自己的服务器或 VPS 21 | - 📍 **适合场景**:企业使用、功能要求高、有服务器资源 22 | 23 | ### 方案三:混合部署 24 | - 使用 Cloudflare Workers 作为前端 25 | - 自建服务器提供 API 服务 26 | - 获得两者优势 27 | 28 | --- 29 | 30 | ## 📖 详细部署指南 31 | 32 | ### 🌥️ Cloudflare Workers 版本(本文档) 33 | 继续阅读本文档,了解 Workers 版本的部署方法 34 | 35 | ### 🏗️ 前后端分离版本 36 | 查看 [`self-hosted/README.md`](./self-hosted/README.md) 了解自托管部署方法 37 | 38 | --- 39 | 40 | ## 主要特性 41 | **初级版本** 42 | 43 | - 清晰展示域名列表及其关键信息:域名状态、注册商、注册日期和过期日期 44 | - 可视化呈现域名使用进度条 45 | - 自动计算并显示域名剩余有效天数 46 | - 响应式设计,完美适配桌面和移动设备 47 | - 轻量级实现,快速加载 48 | - **支持输入自定义域名** 49 | 50 | **高级版本** 51 | - 清晰展示域名列表及其关键信息:域名状态、注册商、注册日期、过期日期和**剩余天数** 52 | - 可视化呈现域名使用进度条 53 | - 自动计算并显示域名剩余有效天数 54 | - 响应式设计,完美适配桌面和移动设备 55 | - 轻量级实现,快速加载 56 | - **UI进一步美化,风格统一** 57 | - **前台和后台分离,支持密码保护** 58 | - **通过 Cloudflare API 自动获取域名列表** 59 | - **集成自建 WHOIS 代理服务,自动获取顶级域名信息、二级域名的注册日期** 60 | - **支持手动编辑二级域名信息** 61 | - **支持输入自定义域名** 62 | 63 | ## 技术实现 64 | - 前端:HTML5, CSS3, JavaScript 65 | - 后端:Cloudflare Workers, KV 存储 66 | - API 集成:Cloudflare API, 自建 WHOIS 代理服务 67 | 68 | ## 个性化部分 69 | - 可修改 `CUSTOM_TITLE` 变量来自定义面板标题 70 | - 可以绑定自定义域名到 Worker,以提高访问稳定性 71 | 72 | # 🌥️ Cloudflare Workers 部署方案 73 | 74 | ## DomainKeeper - 初级版本,只能自定义输入,更灵活,但不高效,适用于少数域名 75 | 76 | ## 快速部署 77 | 78 | - 登录您的Cloudflare账户 79 | - 创建新的Worker 80 | - 将 `index.js` 的内容复制到Worker编辑器,编辑 `DOMAINS` 数组,添加您的域名信息: 81 | ```javascript 82 | const DOMAINS = [ 83 | { domain: "example.com", registrationDate: "2022-01-01", expirationDate: "2027-01-01", system: "Cloudflare" }, 84 | // 添加更多域名... 85 | ]; 86 | ``` 87 | - 保存并部署 88 | 89 | ## demo 90 | ![image](https://github.com/ypq123456789/domainkeeper/assets/114487221/546d0a4c-a74b-436c-a42e-1b013ff6e62b) 91 | [demo.0o11.com](http://demo.0o11.com/) 92 | 93 | # DomainKeeper - 高级版本,集成cloudflare的域名信息获取和whois查询功能,大大提升了域名管理的效率和便捷性 94 | 95 | ## 快速部署 96 | 97 | 1. 登录您的 Cloudflare 账户 98 | 2. 创建新的 Worker 99 | 3. 将domainkeeper.js脚本内容复制到 Worker 编辑器 100 | 4. 在脚本顶部配置以下变量: 101 | ```javascript 102 | const CF_API_KEY = "your_cloudflare_api_key"; 103 | const WHOIS_PROXY_URL = "your_whois_proxy_url"; 104 | const ACCESS_PASSWORD = "your_frontend_password"; 105 | const ADMIN_PASSWORD = "your_backend_password"; 106 | ``` 107 | 108 | **CF_API_KEY的获取方式**: 登录自己的cloudflare账号,打开https://dash.cloudflare.com/profile 点击API令牌,创建令牌,读取所有资源-使用模板,继续以显示摘要,创建令牌,复制此令牌,**保存到记事本,之后不会再显示!** 109 | 110 | **WHOIS_PROXY_URL的获取方式**:需要你自建,详见[whois-proxy](https://github.com/ypq123456789/whois-proxy)。**注意,whois-proxy用于本脚本必须绑定域名,不能用IP!假如你的api请求地址是http(s)://你的域名/whois 那么WHOIS_PROXY_URL你只需要填入http(s)://你的域名。** 111 | 112 | 前台密码按需设置,**后台密码必须设置。** 113 | 114 | 5. 创建一个 KV 命名空间,命名为`DOMAIN_INFO`,并将其绑定到 Worker,绑定名称为 `DOMAIN_INFO` 115 | ![image](https://github.com/ypq123456789/domainkeeper/assets/114487221/6d97b4c4-3cfe-4b1f-9423-000348498f8e) 116 | ![image](https://github.com/ypq123456789/domainkeeper/assets/114487221/ff4601b0-5787-4152-ae96-1e79e0e4d817) 117 | 118 | 6. 保存并部署 119 | 120 | ## demo 121 | ![image](https://github.com/ypq123456789/domainkeeper/assets/114487221/0ac1f968-f5f8-498c-888c-af9456a9c6bd) 122 | 123 | ![image](https://github.com/ypq123456789/domainkeeper/assets/114487221/20ebfa4e-8204-4b11-858f-e8b742b22785) 124 | 125 | https://dm.0o11.com/ 126 | 127 | # 🔄 版本对比 128 | 129 | | 功能特性 | Workers 初级版 | Workers 高级版 | 前后端分离版 | 130 | |---------|---------------|---------------|-------------| 131 | | 部署难度 | ⭐ 极简单 | ⭐⭐ 简单 | ⭐⭐⭐ 中等 | 132 | | 功能丰富度 | ⭐⭐ 基础 | ⭐⭐⭐ 较好 | ⭐⭐⭐⭐⭐ 完整 | 133 | | 界面美观度 | ⭐⭐⭐ 良好 | ⭐⭐⭐⭐ 很好 | ⭐⭐⭐⭐⭐ 现代化 | 134 | | 扩展性 | ❌ 有限 | ❌ 有限 | ✅ 完全可扩展 | 135 | | 成本 | 💰 免费 | 💰 免费 | 💰💰 需服务器 | 136 | | 数据控制 | ❌ 依赖CF | ❌ 依赖CF | ✅ 完全控制 | 137 | | API接口 | ❌ 无 | ❌ 无 | ✅ RESTful API | 138 | | 移动端适配 | ✅ 响应式 | ✅ 响应式 | ✅ 完美适配 | 139 | 140 | **选择建议**: 141 | - 🔰 **新手用户**:选择 Workers 初级版,简单快速 142 | - 🚀 **进阶用户**:选择 Workers 高级版,功能更丰富 143 | - 🏢 **企业用户**:选择前后端分离版,功能完整可扩展 144 | 145 | # 其他 146 | ## 贡献指南 147 | 148 | 欢迎通过Issue和Pull Request参与项目改进。如有重大变更,请先提Issue讨论。 149 | 150 | ## 开源协议 151 | 152 | 本项目采用 [MIT 许可证](https://choosealicense.com/licenses/mit/) 153 | 154 | ## Star History 155 | 156 | [![Star History Chart](https://api.star-history.com/svg?repos=ypq123456789/domainkeeper&type=Date)](https://star-history.com/#ypq123456789/domainkeeper&Date) 157 | 158 | ## 交流TG群: 159 | https://t.me/+ydvXl1_OBBBiZWM1 160 | 161 | ## 支持作者 162 | 163 | 非常感谢您对 domainkeeper 项目的兴趣!维护开源项目确实需要大量时间和精力投入。若您认为这个项目为您带来了价值,希望您能考虑给予一些支持,哪怕只是一杯咖啡的费用。 164 | 您的慷慨相助将激励我继续完善这个项目,使其更加实用。它还能让我更专心地参与开源社区的工作。如果您愿意提供赞助,可通过下列渠道: 165 | 166 | - 给该项目点赞 [![给该项目点赞](https://img.shields.io/github/stars/ypq123456789/domainkeeper?style=social)](https://github.com/ypq123456789/domainkeeper) 167 | - 关注我的 Github [![关注我的 Github](https://img.shields.io/github/followers/ypq123456789?style=social)](https://github.com/ypq123456789) 168 | 169 | | 微信 | 支付宝 | 170 | |------|--------| 171 | | ![微信](https://github.com/ypq123456789/TrafficCop/assets/114487221/fb265eef-e624-4429-b14a-afdf5b2ca9c4) | ![支付宝](https://github.com/ypq123456789/TrafficCop/assets/114487221/884b58bd-d76f-4e8f-99f4-cac4b9e97168) | 172 | 173 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/store/modules/domains.js: -------------------------------------------------------------------------------- 1 | import api from '@/utils/api' 2 | 3 | const state = { 4 | domains: [], 5 | loading: false, 6 | stats: null 7 | } 8 | 9 | const getters = { 10 | domains: state => state.domains, 11 | loading: state => state.loading, 12 | stats: state => state.stats, 13 | 14 | // 分类域名 15 | categorizedDomains: state => { 16 | const cfTopLevel = [] 17 | const cfSecondLevelAndCustom = [] 18 | 19 | state.domains.forEach(domain => { 20 | if (domain.system === 'Cloudflare' && domain.domain.split('.').length === 2) { 21 | cfTopLevel.push(domain) 22 | } else { 23 | cfSecondLevelAndCustom.push(domain) 24 | } 25 | }) 26 | 27 | return { 28 | cfTopLevel: cfTopLevel.sort((a, b) => a.domain.localeCompare(b.domain)), 29 | cfSecondLevelAndCustom: cfSecondLevelAndCustom.sort((a, b) => a.domain.localeCompare(b.domain)) 30 | } 31 | }, 32 | 33 | // 统计信息 34 | domainStats: state => { 35 | const total = state.domains.length 36 | const expired = state.domains.filter(d => d.daysRemaining <= 0).length 37 | const expiring = state.domains.filter(d => d.daysRemaining > 0 && d.daysRemaining <= 30).length 38 | const normal = total - expired - expiring 39 | 40 | return { 41 | total, 42 | expired, 43 | expiring, 44 | normal 45 | } 46 | } 47 | } 48 | 49 | const mutations = { 50 | SET_LOADING(state, loading) { 51 | state.loading = loading 52 | }, 53 | 54 | SET_DOMAINS(state, domains) { 55 | state.domains = domains 56 | }, 57 | 58 | SET_STATS(state, stats) { 59 | state.stats = stats 60 | }, 61 | 62 | ADD_DOMAIN(state, domain) { 63 | state.domains.push(domain) 64 | }, 65 | 66 | UPDATE_DOMAIN(state, updatedDomain) { 67 | const index = state.domains.findIndex(d => d.id === updatedDomain.id) 68 | if (index !== -1) { 69 | state.domains.splice(index, 1, updatedDomain) 70 | } 71 | }, 72 | 73 | REMOVE_DOMAIN(state, domainId) { 74 | const index = state.domains.findIndex(d => d.id === domainId) 75 | if (index !== -1) { 76 | state.domains.splice(index, 1) 77 | } 78 | } 79 | } 80 | 81 | const actions = { 82 | async fetchDomains({ commit }) { 83 | commit('SET_LOADING', true) 84 | try { 85 | const domainsResponse = await api.get('/domains') 86 | commit('SET_DOMAINS', domainsResponse.data.domains) 87 | return { success: true } 88 | } catch (error) { 89 | return { 90 | success: false, 91 | message: error.response?.data?.error || '获取域名列表失败' 92 | } 93 | } finally { 94 | commit('SET_LOADING', false) 95 | } 96 | }, 97 | 98 | async fetchStats({ commit }) { 99 | try { 100 | const response = await api.get('/domains/stats/overview') 101 | commit('SET_STATS', response.data.stats) 102 | return { success: true } 103 | } catch (error) { 104 | return { 105 | success: false, 106 | message: error.response?.data?.error || '获取统计信息失败' 107 | } 108 | } 109 | }, 110 | 111 | async addDomain({ commit }, domainData) { 112 | try { 113 | await api.post('/domains', domainData) 114 | // 重新获取域名列表 115 | const domainsResponse = await api.get('/domains') 116 | commit('SET_DOMAINS', domainsResponse.data.domains) 117 | return { success: true, message: '域名添加成功' } 118 | } catch (error) { 119 | return { 120 | success: false, 121 | message: error.response?.data?.error || '添加域名失败' 122 | } 123 | } 124 | }, 125 | 126 | async updateDomain({ commit }, { id, domainData }) { 127 | try { 128 | await api.put(`/domains/${id}`, domainData) 129 | // 重新获取域名列表 130 | const domainsResponse = await api.get('/domains') 131 | commit('SET_DOMAINS', domainsResponse.data.domains) 132 | return { success: true, message: '域名更新成功' } 133 | } catch (error) { 134 | return { 135 | success: false, 136 | message: error.response?.data?.error || '更新域名失败' 137 | } 138 | } 139 | }, 140 | 141 | async deleteDomain({ commit }, domainId) { 142 | try { 143 | await api.delete(`/domains/${domainId}`) 144 | commit('REMOVE_DOMAIN', domainId) 145 | return { success: true, message: '域名删除成功' } 146 | } catch (error) { 147 | return { 148 | success: false, 149 | message: error.response?.data?.error || '删除域名失败' 150 | } 151 | } 152 | }, 153 | 154 | async syncCloudflare({ commit }) { 155 | try { 156 | const response = await api.post('/domains/sync-cloudflare') 157 | // 重新获取域名列表 158 | const domainsResponse = await api.get('/domains') 159 | commit('SET_DOMAINS', domainsResponse.data.domains) 160 | return { 161 | success: true, 162 | message: response.data.message, 163 | stats: response.data.stats 164 | } 165 | } catch (error) { 166 | return { 167 | success: false, 168 | message: error.response?.data?.error || '同步Cloudflare失败' 169 | } 170 | } 171 | }, 172 | 173 | async updateWhois({ commit }, domain) { 174 | try { 175 | await api.post(`/domains/${domain}/whois`) 176 | // 重新获取域名列表 177 | const domainsResponse = await api.get('/domains') 178 | commit('SET_DOMAINS', domainsResponse.data.domains) 179 | return { 180 | success: true, 181 | message: 'WHOIS信息更新成功' 182 | } 183 | } catch (error) { 184 | return { 185 | success: false, 186 | message: error.response?.data?.error || 'WHOIS更新失败' 187 | } 188 | } 189 | } 190 | } 191 | 192 | export default { 193 | namespaced: true, 194 | state, 195 | getters, 196 | mutations, 197 | actions 198 | } 199 | -------------------------------------------------------------------------------- /self-hosted/README.md: -------------------------------------------------------------------------------- 1 | # DomainKeeper 前后端分离版本部署指南 2 | 3 | ## 项目介绍 4 | 5 | DomainKeeper 前后端分离版本是一个现代化的域名管理系统,提供了Web界面来管理和监控您的域名。本版本支持自托管部署,具有以下特性: 6 | 7 | ### 主要功能 8 | 9 | - 🌐 **域名管理**: 添加、编辑、删除域名记录 10 | - 🔄 **Cloudflare集成**: 自动同步Cloudflare域名 11 | - 🔍 **WHOIS查询**: 获取域名注册信息 12 | - 📊 **可视化仪表板**: 域名状态统计图表 13 | - ⏰ **过期提醒**: 域名到期状态监控 14 | - 🔐 **权限管理**: 前台访问和后台管理分离 15 | - 🐳 **Docker支持**: 一键部署,易于维护 16 | 17 | ### 技术栈 18 | 19 | **后端**: 20 | - Node.js + Express 21 | - SQLite 数据库 22 | - JWT 认证 23 | - 定时任务同步 24 | 25 | **前端**: 26 | - Vue.js 3 27 | - Element Plus UI 28 | - ECharts 图表 29 | - Responsive 响应式设计 30 | 31 | ## 快速开始 32 | 33 | ### 方式一:Docker Compose 部署(推荐) 34 | 35 | #### 1. 准备环境 36 | 37 | 确保您的服务器已安装: 38 | - Docker 39 | - Docker Compose 40 | 41 | #### 2. 下载项目 42 | 43 | ```bash 44 | git clone 45 | cd domainkeeper/self-hosted 46 | ``` 47 | 48 | #### 3. 配置环境变量 49 | 50 | ```bash 51 | # 复制环境变量模板 52 | cp .env.example .env 53 | 54 | # 编辑配置文件 55 | nano .env 56 | ``` 57 | 58 | **重要配置说明**: 59 | 60 | ```bash 61 | # 必须修改的配置 62 | JWT_SECRET=your_super_secret_jwt_key_change_this_in_production 63 | ADMIN_PASSWORD=your_secure_admin_password 64 | 65 | # Cloudflare配置(可选) 66 | CF_API_TOKEN=your_cloudflare_api_token 67 | 68 | # WHOIS服务配置(可选) 69 | WHOIS_PROXY_URL=https://whois.0o11.com 70 | 71 | # 前台访问密码(可选) 72 | ACCESS_PASSWORD= 73 | ``` 74 | 75 | #### 4. 启动服务 76 | 77 | ```bash 78 | # 构建并启动所有服务 79 | docker-compose up -d 80 | 81 | # 查看运行状态 82 | docker-compose ps 83 | 84 | # 查看日志 85 | docker-compose logs -f 86 | ``` 87 | 88 | #### 5. 访问系统 89 | 90 | - **前台地址**: http://your-server-ip 91 | - **管理后台**: 点击页面上的"管理后台"按钮 92 | 93 | ### 方式二:手动部署 94 | 95 | #### 后端部署 96 | 97 | 1. **环境准备** 98 | ```bash 99 | # 安装 Node.js 18+ 100 | curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - 101 | sudo apt-get install -y nodejs 102 | 103 | # 进入后端目录 104 | cd backend 105 | ``` 106 | 107 | 2. **安装依赖** 108 | ```bash 109 | npm install 110 | ``` 111 | 112 | 3. **配置环境** 113 | ```bash 114 | cp .env.example .env 115 | nano .env 116 | ``` 117 | 118 | 4. **启动服务** 119 | ```bash 120 | # 开发模式 121 | npm run dev 122 | 123 | # 生产模式 124 | npm start 125 | ``` 126 | 127 | #### 前端部署 128 | 129 | 1. **构建前端** 130 | ```bash 131 | cd frontend 132 | npm install 133 | npm run build 134 | ``` 135 | 136 | 2. **配置Nginx** 137 | ```bash 138 | # 复制构建文件到web目录 139 | sudo cp -r dist/* /var/www/html/ 140 | 141 | # 配置Nginx代理 142 | sudo nano /etc/nginx/sites-available/domainkeeper 143 | ``` 144 | 145 | Nginx配置示例: 146 | ```nginx 147 | server { 148 | listen 80; 149 | server_name your-domain.com; 150 | 151 | location / { 152 | root /var/www/html; 153 | try_files $uri $uri/ /index.html; 154 | } 155 | 156 | location /api/ { 157 | proxy_pass http://localhost:3001; 158 | proxy_set_header Host $host; 159 | proxy_set_header X-Real-IP $remote_addr; 160 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 161 | } 162 | } 163 | ``` 164 | 165 | ## 配置说明 166 | 167 | ### 环境变量详解 168 | 169 | | 变量名 | 说明 | 默认值 | 必需 | 170 | |--------|------|---------|------| 171 | | `JWT_SECRET` | JWT加密密钥 | - | ✅ | 172 | | `ADMIN_USERNAME` | 管理员用户名 | admin | ✅ | 173 | | `ADMIN_PASSWORD` | 管理员密码 | admin123 | ✅ | 174 | | `ACCESS_PASSWORD` | 前台访问密码 | 空 | ❌ | 175 | | `CF_API_TOKEN` | Cloudflare API Token | - | ❌ | 176 | | `WHOIS_PROXY_URL` | WHOIS代理服务地址 | - | ❌ | 177 | | `SYNC_INTERVAL` | 同步间隔(分钟) | 60 | ❌ | 178 | | `FRONTEND_URL` | 前端地址 | http://localhost | ❌ | 179 | | `CUSTOM_TITLE` | 自定义标题 | 我的域名管理 | ❌ | 180 | 181 | ### Cloudflare API Token 获取 182 | 183 | 1. 访问 [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) 184 | 2. 点击 "Create Token" 185 | 3. 选择 "Zone:Read" 模板 186 | 4. 配置权限: 187 | - Zone Resources: Include - All zones 188 | - Zone Permissions: Zone:Read 189 | 5. 复制生成的token到配置文件 190 | 191 | ### WHOIS 代理服务 192 | 193 | 如需WHOIS功能,可以: 194 | 1. 使用公共服务: `https://whois.0o11.com` 195 | 2. 自建WHOIS代理: 参考 [whois-proxy项目](https://github.com/ypq123456789/whois-proxy) 196 | 197 | ## 使用指南 198 | 199 | ### 首次登录 200 | 201 | 1. **前台访问**: 202 | - 如果设置了`ACCESS_PASSWORD`,输入前台密码 203 | - 如果没有设置密码,直接点击"进入系统" 204 | 205 | 2. **管理后台**: 206 | - 使用管理员用户名和密码登录 207 | - 可以管理域名、同步数据、查看统计信息 208 | 209 | ### 域名管理 210 | 211 | 1. **同步Cloudflare域名**: 212 | - 在管理后台点击"同步Cloudflare"按钮 213 | - 系统会自动获取所有Zone信息 214 | 215 | 2. **手动添加域名**: 216 | - 点击"添加域名"按钮 217 | - 填写域名信息并保存 218 | 219 | 3. **编辑域名信息**: 220 | - 点击域名行的"编辑"按钮 221 | - 修改注册商、日期等信息 222 | 223 | ### 自动化功能 224 | 225 | - **定时同步**: 系统会根据`SYNC_INTERVAL`设置自动同步域名 226 | - **WHOIS更新**: 自动更新顶级域名的WHOIS信息 227 | - **过期监控**: 自动计算剩余天数并提供状态指示 228 | 229 | ## 维护与监控 230 | 231 | ### 日志查看 232 | 233 | ```bash 234 | # Docker方式 235 | docker-compose logs -f backend 236 | docker-compose logs -f frontend 237 | 238 | # 手动部署方式 239 | tail -f backend/logs/combined.log 240 | ``` 241 | 242 | ### 数据备份 243 | 244 | ```bash 245 | # 备份数据库 246 | docker-compose exec backend cp /app/data/domains.db /app/backup-$(date +%Y%m%d).db 247 | 248 | # 或直接复制数据卷 249 | docker cp domainkeeper-backend:/app/data ./backup/ 250 | ``` 251 | 252 | ### 更新升级 253 | 254 | ```bash 255 | # 停止服务 256 | docker-compose down 257 | 258 | # 拉取最新代码 259 | git pull 260 | 261 | # 重新构建并启动 262 | docker-compose up -d --build 263 | ``` 264 | 265 | ## 故障排除 266 | 267 | ### 常见问题 268 | 269 | **Q: 无法访问管理后台** 270 | A: 检查管理员密码配置,确保`ADMIN_PASSWORD`已正确设置 271 | 272 | **Q: Cloudflare同步失败** 273 | A: 验证`CF_API_TOKEN`是否正确,检查token权限 274 | 275 | **Q: WHOIS查询不工作** 276 | A: 检查`WHOIS_PROXY_URL`配置,确保服务可访问 277 | 278 | **Q: 前端无法连接后端** 279 | A: 检查网络配置,确保API地址正确 280 | 281 | ### 端口检查 282 | 283 | ```bash 284 | # 检查端口占用 285 | netstat -tlnp | grep :3001 # 后端端口 286 | netstat -tlnp | grep :80 # 前端端口 287 | 288 | # 测试API连接 289 | curl http://localhost:3001/api/health 290 | ``` 291 | 292 | ### 性能优化 293 | 294 | 1. **数据库优化**: 295 | - 定期清理过期WHOIS缓存 296 | - 监控数据库文件大小 297 | 298 | 2. **内存优化**: 299 | - 调整Node.js内存限制 300 | - 监控Docker容器资源使用 301 | 302 | ## 安全建议 303 | 304 | 1. **密码安全**: 305 | - 使用强密码 306 | - 定期更换密码 307 | - 启用HTTPS 308 | 309 | 2. **网络安全**: 310 | - 使用防火墙限制端口访问 311 | - 配置反向代理 312 | - 启用访问日志 313 | 314 | 3. **数据安全**: 315 | - 定期备份数据 316 | - 限制数据库文件权限 317 | - 监控异常访问 318 | 319 | ## 开发与扩展 320 | 321 | ### 本地开发 322 | 323 | ```bash 324 | # 后端开发 325 | cd backend 326 | npm install 327 | npm run dev 328 | 329 | # 前端开发 330 | cd frontend 331 | npm install 332 | npm run serve 333 | ``` 334 | 335 | ### API 接口 336 | 337 | 系统提供RESTful API接口: 338 | 339 | - `GET /api/domains` - 获取域名列表 340 | - `POST /api/domains` - 添加域名 341 | - `PUT /api/domains/:id` - 更新域名 342 | - `DELETE /api/domains/:id` - 删除域名 343 | - `POST /api/domains/sync-cloudflare` - 同步Cloudflare 344 | - `GET /api/whois/:domain` - 查询WHOIS信息 345 | 346 | ### 自定义功能 347 | 348 | 可以通过修改源码添加自定义功能: 349 | 350 | 1. **后端**: 在`routes/`目录添加新的API路由 351 | 2. **前端**: 在`src/views/`目录添加新的页面组件 352 | 3. **数据库**: 修改`utils/database.js`中的表结构 353 | 354 | ## 许可证 355 | 356 | 本项目基于 MIT 许可证开源。 357 | 358 | ## 支持与反馈 359 | 360 | 如果您遇到问题或有建议,请: 361 | 362 | 1. 查看本文档的故障排除部分 363 | 2. 检查项目的GitHub Issues 364 | 3. 提交新的Issue报告问题 365 | 366 | --- 367 | 368 | **注意**: 在生产环境中使用前,请务必修改默认密码和JWT密钥,并根据需要配置HTTPS。 369 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 216 | 217 | 301 | -------------------------------------------------------------------------------- /self-hosted/backend/routes/domains.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const db = require('../utils/database'); 3 | const cloudflareService = require('../services/cloudflareService'); 4 | const whoisService = require('../services/whoisService'); 5 | const authMiddleware = require('../middleware/auth'); 6 | 7 | const router = express.Router(); 8 | 9 | // 获取域名列表 10 | router.get('/', authMiddleware, async (req, res) => { 11 | try { 12 | const domains = await db.all(` 13 | SELECT d.*, w.whois_data, w.cached_at 14 | FROM domains d 15 | LEFT JOIN whois_cache w ON d.domain = w.domain 16 | ORDER BY d.domain 17 | `); 18 | 19 | const domainsWithInfo = domains.map(domain => { 20 | const today = new Date(); 21 | const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null; 22 | const registrationDate = domain.registration_date ? new Date(domain.registration_date) : null; 23 | 24 | let daysRemaining = 'N/A'; 25 | let progressPercentage = 0; 26 | 27 | if (expirationDate) { 28 | daysRemaining = Math.ceil((expirationDate - today) / (1000 * 60 * 60 * 24)); 29 | 30 | if (registrationDate && expirationDate) { 31 | const totalDays = Math.ceil((expirationDate - registrationDate) / (1000 * 60 * 60 * 24)); 32 | const daysElapsed = Math.ceil((today - registrationDate) / (1000 * 60 * 60 * 24)); 33 | progressPercentage = Math.min(100, Math.max(0, (daysElapsed / totalDays) * 100)); 34 | } 35 | } 36 | 37 | return { 38 | id: domain.id, 39 | domain: domain.domain, 40 | system: domain.system, 41 | registrar: domain.registrar, 42 | registrationDate: domain.registration_date, 43 | expirationDate: domain.expiration_date, 44 | zoneId: domain.zone_id, 45 | isCustom: Boolean(domain.is_custom), 46 | daysRemaining, 47 | progressPercentage: Math.round(progressPercentage * 100) / 100, 48 | whoisData: domain.whois_data ? JSON.parse(domain.whois_data) : null, 49 | whoisCachedAt: domain.cached_at, 50 | createdAt: domain.created_at, 51 | updatedAt: domain.updated_at 52 | }; 53 | }); 54 | 55 | res.json({ 56 | success: true, 57 | domains: domainsWithInfo, 58 | total: domainsWithInfo.length 59 | }); 60 | } catch (error) { 61 | console.error('获取域名列表错误:', error); 62 | res.status(500).json({ error: '获取域名列表失败' }); 63 | } 64 | }); 65 | 66 | // 获取单个域名信息 67 | router.get('/:domain', authMiddleware, async (req, res) => { 68 | try { 69 | const { domain } = req.params; 70 | 71 | const domainInfo = await db.get(` 72 | SELECT d.*, w.whois_data, w.cached_at 73 | FROM domains d 74 | LEFT JOIN whois_cache w ON d.domain = w.domain 75 | WHERE d.domain = ? 76 | `, [domain]); 77 | 78 | if (!domainInfo) { 79 | return res.status(404).json({ error: '域名不存在' }); 80 | } 81 | 82 | res.json({ 83 | success: true, 84 | domain: { 85 | ...domainInfo, 86 | whoisData: domainInfo.whois_data ? JSON.parse(domainInfo.whois_data) : null, 87 | isCustom: Boolean(domainInfo.is_custom) 88 | } 89 | }); 90 | } catch (error) { 91 | console.error('获取域名信息错误:', error); 92 | res.status(500).json({ error: '获取域名信息失败' }); 93 | } 94 | }); 95 | 96 | // 添加域名 97 | router.post('/', authMiddleware('admin'), async (req, res) => { 98 | try { 99 | const { domain, system, registrar, registrationDate, expirationDate } = req.body; 100 | 101 | if (!domain) { 102 | return res.status(400).json({ error: '域名不能为空' }); 103 | } 104 | 105 | // 检查域名是否已存在 106 | const existing = await db.get('SELECT id FROM domains WHERE domain = ?', [domain]); 107 | if (existing) { 108 | return res.status(400).json({ error: '域名已存在' }); 109 | } 110 | 111 | const result = await db.run( 112 | 'INSERT INTO domains (domain, system, registrar, registration_date, expiration_date, is_custom) VALUES (?, ?, ?, ?, ?, 1)', 113 | [domain, system, registrar, registrationDate, expirationDate] 114 | ); 115 | 116 | res.json({ 117 | success: true, 118 | message: '域名添加成功', 119 | id: result.id 120 | }); 121 | } catch (error) { 122 | console.error('添加域名错误:', error); 123 | res.status(500).json({ error: '添加域名失败' }); 124 | } 125 | }); 126 | 127 | // 更新域名信息 128 | router.put('/:id', authMiddleware('admin'), async (req, res) => { 129 | try { 130 | const { id } = req.params; 131 | const { system, registrar, registrationDate, expirationDate } = req.body; 132 | 133 | const result = await db.run( 134 | 'UPDATE domains SET system = ?, registrar = ?, registration_date = ?, expiration_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', 135 | [system, registrar, registrationDate, expirationDate, id] 136 | ); 137 | 138 | if (result.changes === 0) { 139 | return res.status(404).json({ error: '域名不存在' }); 140 | } 141 | 142 | res.json({ 143 | success: true, 144 | message: '域名信息更新成功' 145 | }); 146 | } catch (error) { 147 | console.error('更新域名信息错误:', error); 148 | res.status(500).json({ error: '更新域名信息失败' }); 149 | } 150 | }); 151 | 152 | // 删除域名 153 | router.delete('/:id', authMiddleware('admin'), async (req, res) => { 154 | try { 155 | const { id } = req.params; 156 | 157 | const result = await db.run('DELETE FROM domains WHERE id = ?', [id]); 158 | 159 | if (result.changes === 0) { 160 | return res.status(404).json({ error: '域名不存在' }); 161 | } 162 | 163 | res.json({ 164 | success: true, 165 | message: '域名删除成功' 166 | }); 167 | } catch (error) { 168 | console.error('删除域名错误:', error); 169 | res.status(500).json({ error: '删除域名失败' }); 170 | } 171 | }); 172 | 173 | // 同步Cloudflare域名 174 | router.post('/sync-cloudflare', authMiddleware('admin'), async (req, res) => { 175 | try { 176 | const result = await cloudflareService.syncDomains(); 177 | 178 | // 记录同步日志 179 | await db.run( 180 | 'INSERT INTO sync_logs (sync_type, status, message, domains_count) VALUES (?, ?, ?, ?)', 181 | ['cloudflare', result.success ? 'success' : 'failed', result.message, result.count || 0] 182 | ); 183 | 184 | if (result.success) { 185 | res.json({ 186 | success: true, 187 | message: `同步成功,共处理 ${result.count} 个域名`, 188 | domains: result.domains 189 | }); 190 | } else { 191 | res.status(500).json({ 192 | error: '同步失败', 193 | message: result.message 194 | }); 195 | } 196 | } catch (error) { 197 | console.error('同步Cloudflare域名错误:', error); 198 | res.status(500).json({ error: '同步失败' }); 199 | } 200 | }); 201 | 202 | // 更新WHOIS信息 203 | router.post('/:domain/whois', authMiddleware('admin'), async (req, res) => { 204 | try { 205 | const { domain } = req.params; 206 | 207 | // 检查域名是否存在 208 | const domainExists = await db.get('SELECT id FROM domains WHERE domain = ?', [domain]); 209 | if (!domainExists) { 210 | return res.status(404).json({ error: '域名不存在' }); 211 | } 212 | 213 | const whoisResult = await whoisService.fetchWhoisInfo(domain); 214 | 215 | if (whoisResult.success) { 216 | // 更新域名信息(如果WHOIS查询成功) 217 | await db.run( 218 | 'UPDATE domains SET registrar = ?, registration_date = ?, expiration_date = ?, updated_at = CURRENT_TIMESTAMP WHERE domain = ?', 219 | [whoisResult.data.registrar, whoisResult.data.registrationDate, whoisResult.data.expirationDate, domain] 220 | ); 221 | 222 | // 缓存WHOIS数据 223 | const expiresAt = new Date(); 224 | expiresAt.setDate(expiresAt.getDate() + 7); // 7天后过期 225 | 226 | await db.run( 227 | 'INSERT OR REPLACE INTO whois_cache (domain, whois_data, expires_at) VALUES (?, ?, ?)', 228 | [domain, JSON.stringify(whoisResult.data), expiresAt.toISOString()] 229 | ); 230 | 231 | res.json({ 232 | success: true, 233 | message: 'WHOIS信息更新成功', 234 | data: whoisResult.data 235 | }); 236 | } else { 237 | res.status(500).json({ 238 | error: 'WHOIS信息获取失败', 239 | message: whoisResult.message 240 | }); 241 | } 242 | } catch (error) { 243 | console.error('更新WHOIS信息错误:', error); 244 | res.status(500).json({ error: 'WHOIS信息更新失败' }); 245 | } 246 | }); 247 | 248 | // 获取统计信息 249 | router.get('/stats/overview', authMiddleware, async (req, res) => { 250 | try { 251 | const totalDomains = await db.get('SELECT COUNT(*) as count FROM domains'); 252 | const customDomains = await db.get('SELECT COUNT(*) as count FROM domains WHERE is_custom = 1'); 253 | const cfDomains = await db.get('SELECT COUNT(*) as count FROM domains WHERE system = "Cloudflare"'); 254 | 255 | // 计算即将过期的域名(30天内) 256 | const today = new Date(); 257 | const thirtyDaysLater = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); 258 | const expiringDomains = await db.get( 259 | 'SELECT COUNT(*) as count FROM domains WHERE expiration_date BETWEEN ? AND ?', 260 | [today.toISOString().split('T')[0], thirtyDaysLater.toISOString().split('T')[0]] 261 | ); 262 | 263 | const expiredDomains = await db.get( 264 | 'SELECT COUNT(*) as count FROM domains WHERE expiration_date < ?', 265 | [today.toISOString().split('T')[0]] 266 | ); 267 | 268 | res.json({ 269 | success: true, 270 | stats: { 271 | total: totalDomains.count, 272 | custom: customDomains.count, 273 | cloudflare: cfDomains.count, 274 | expiring: expiringDomains.count, 275 | expired: expiredDomains.count 276 | } 277 | }); 278 | } catch (error) { 279 | console.error('获取统计信息错误:', error); 280 | res.status(500).json({ error: '获取统计信息失败' }); 281 | } 282 | }); 283 | 284 | module.exports = router; 285 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 122 | 123 | 290 | 291 | 462 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/views/Domains.vue: -------------------------------------------------------------------------------- 1 | 215 | 216 | 315 | 316 | 511 | -------------------------------------------------------------------------------- /self-hosted/frontend/src/views/Admin.vue: -------------------------------------------------------------------------------- 1 | 282 | 283 | 597 | 598 | 744 | -------------------------------------------------------------------------------- /domainkeeper.js: -------------------------------------------------------------------------------- 1 | // 在文件顶部添加版本信息后台密码(不可为空) 2 | const VERSION = "1.7.0"; 3 | 4 | // 自定义标题 5 | const CUSTOM_TITLE = "培根的玉米大全"; 6 | 7 | // 在这里设置你的 Cloudflare API Token 8 | const CF_API_KEY = ""; 9 | 10 | // 自建 WHOIS 代理服务地址 11 | const WHOIS_PROXY_URL = "https://whois.0o11.com"; 12 | 13 | // 访问密码(可为空) 14 | const ACCESS_PASSWORD = ""; 15 | 16 | // 后台密码(不可为空) 17 | const ADMIN_PASSWORD = ""; 18 | 19 | // KV 命名空间绑定名称 20 | const KV_NAMESPACE = DOMAIN_INFO; 21 | 22 | // footerHTML 23 | const footerHTML = ` 24 | 37 | `; 38 | 39 | addEventListener('fetch', event => { 40 | event.respondWith(handleRequest(event.request)) 41 | }); 42 | 43 | async function handleRequest(request) { 44 | // 清理KV中的错误内容 45 | await cleanupKV(); 46 | const url = new URL(request.url); 47 | const path = url.pathname; 48 | 49 | if (path === "/api/manual-query") { 50 | return handleManualQuery(request); 51 | } 52 | 53 | if (path === "/") { 54 | return handleFrontend(request); 55 | } else if (path === "/admin") { 56 | return handleAdmin(request); 57 | } else if (path === "/api/update") { 58 | return handleApiUpdate(request); 59 | } else if (path === "/login") { 60 | return handleLogin(request); 61 | } else if (path === "/admin-login") { 62 | return handleAdminLogin(request); 63 | } else if (path.startsWith("/whois/")) { 64 | const domain = path.split("/")[2]; 65 | return handleWhoisRequest(domain); 66 | } else { 67 | return new Response("Not Found", { status: 404 }); 68 | } 69 | } 70 | 71 | async function handleManualQuery(request) { 72 | if (request.method !== "POST") { 73 | return new Response("Method Not Allowed", { status: 405 }); 74 | } 75 | 76 | const data = await request.json(); 77 | const { domain, apiKey } = data; 78 | 79 | try { 80 | const whoisInfo = await fetchWhoisInfo(domain, apiKey); 81 | await cacheWhoisInfo(domain, whoisInfo); 82 | return new Response(JSON.stringify(whoisInfo), { 83 | headers: { 'Content-Type': 'application/json' } 84 | }); 85 | } catch (error) { 86 | return new Response(JSON.stringify({ error: error.message }), { 87 | status: 500, 88 | headers: { 'Content-Type': 'application/json' } 89 | }); 90 | } 91 | } 92 | 93 | async function cleanupKV() { 94 | const list = await KV_NAMESPACE.list(); 95 | for (const key of list.keys) { 96 | const value = await KV_NAMESPACE.get(key.name); 97 | if (value) { 98 | try { 99 | const { data } = JSON.parse(value); 100 | if (data.whoisError) { 101 | await KV_NAMESPACE.delete(key.name); 102 | } 103 | } catch (error) { 104 | console.error(`Error parsing data for ${key.name}:`, error); 105 | } 106 | } 107 | } 108 | } 109 | 110 | async function handleFrontend(request) { 111 | const cookie = request.headers.get("Cookie"); 112 | if (ACCESS_PASSWORD && (!cookie || !cookie.includes(`access_token=${ACCESS_PASSWORD}`))) { 113 | return Response.redirect(`${new URL(request.url).origin}/login`, 302); 114 | } 115 | 116 | console.log("Fetching Cloudflare domains info..."); 117 | const domains = await fetchCloudflareDomainsInfo(); 118 | console.log("Cloudflare domains:", domains); 119 | 120 | console.log("Fetching domain info..."); 121 | const domainsWithInfo = await fetchDomainInfo(domains); 122 | console.log("Domains with info:", domainsWithInfo); 123 | 124 | return new Response(generateHTML(domainsWithInfo, false), { 125 | headers: { 'Content-Type': 'text/html' }, 126 | }); 127 | } 128 | 129 | async function handleAdmin(request) { 130 | const cookie = request.headers.get("Cookie"); 131 | if (!cookie || !cookie.includes(`admin_token=${ADMIN_PASSWORD}`)) { 132 | return Response.redirect(`${new URL(request.url).origin}/admin-login`, 302); 133 | } 134 | 135 | const domains = await fetchCloudflareDomainsInfo(); 136 | const domainsWithInfo = await fetchDomainInfo(domains); 137 | return new Response(generateHTML(domainsWithInfo, true), { 138 | headers: { 'Content-Type': 'text/html' }, 139 | }); 140 | } 141 | 142 | async function handleLogin(request) { 143 | if (request.method === "POST") { 144 | const formData = await request.formData(); 145 | const password = formData.get("password"); 146 | 147 | console.log("Entered password:", password); 148 | console.log("Expected password:", ACCESS_PASSWORD); 149 | 150 | if (password === ACCESS_PASSWORD) { 151 | return new Response("Login successful", { 152 | status: 302, 153 | headers: { 154 | "Location": "/", 155 | "Set-Cookie": `access_token=${ACCESS_PASSWORD}; HttpOnly; Path=/; SameSite=Strict` 156 | } 157 | }); 158 | } else { 159 | return new Response(generateLoginHTML("前台登录", "/login", "密码错误,请重试。"), { 160 | headers: { "Content-Type": "text/html" }, 161 | status: 401 162 | }); 163 | } 164 | } 165 | return new Response(generateLoginHTML("前台登录", "/login"), { 166 | headers: { "Content-Type": "text/html" } 167 | }); 168 | } 169 | 170 | async function handleAdminLogin(request) { 171 | console.log("Handling admin login request"); 172 | console.log("Request method:", request.method); 173 | 174 | if (request.method === "POST") { 175 | console.log("Processing POST request for admin login"); 176 | const formData = await request.formData(); 177 | console.log("Form data:", formData); 178 | const password = formData.get("password"); 179 | console.log("Entered admin password:", password); 180 | console.log("Expected admin password:", ADMIN_PASSWORD); 181 | 182 | if (password === ADMIN_PASSWORD) { 183 | return new Response("Admin login successful", { 184 | status: 302, 185 | headers: { 186 | "Location": "/admin", 187 | "Set-Cookie": `admin_token=${ADMIN_PASSWORD}; HttpOnly; Path=/; SameSite=Strict` 188 | } 189 | }); 190 | } else { 191 | return new Response(generateLoginHTML("后台登录", "/admin-login", "密码错误,请重试。"), { 192 | headers: { "Content-Type": "text/html" }, 193 | status: 401 194 | }); 195 | } 196 | } 197 | 198 | return new Response(generateLoginHTML("后台登录", "/admin-login"), { 199 | headers: { "Content-Type": "text/html" } 200 | }); 201 | } 202 | 203 | async function handleApiUpdate(request) { 204 | if (request.method !== "POST") { 205 | return new Response("Method Not Allowed", { status: 405 }); 206 | } 207 | 208 | const auth = request.headers.get("Authorization"); 209 | if (!auth || auth !== `Basic ${btoa(`:${ADMIN_PASSWORD}`)}`) { 210 | return new Response("Unauthorized", { status: 401 }); 211 | } 212 | 213 | try { 214 | const data = await request.json(); 215 | const { action, domain, system, registrar, registrationDate, expirationDate } = data; 216 | 217 | if (action === 'delete') { 218 | // 删除自定义域名 219 | await KV_NAMESPACE.delete(`whois_${domain}`); 220 | } else if (action === 'update-whois') { 221 | // 更新 WHOIS 信息 222 | const whoisInfo = await fetchWhoisInfo(domain); 223 | await cacheWhoisInfo(domain, whoisInfo); 224 | } else if (action === 'add') { 225 | // 添加新域名 226 | const newDomainInfo = { 227 | domain, 228 | system, 229 | registrar, 230 | registrationDate, 231 | expirationDate, 232 | isCustom: true 233 | }; 234 | await cacheWhoisInfo(domain, newDomainInfo); 235 | } else if (action === 'reset-custom') { 236 | // 重置域名的自定义标记 237 | const domainInfo = await getCachedWhoisInfo(domain); 238 | if (domainInfo) { 239 | domainInfo.isCustom = false; 240 | await cacheWhoisInfo(domain, domainInfo); 241 | } 242 | } else if (action === 'get-props') { 243 | // 获取域名属性 244 | const domainInfo = await getCachedWhoisInfo(domain); 245 | if (domainInfo) { 246 | return new Response(JSON.stringify({ 247 | success: true, 248 | props: domainInfo 249 | }), { 250 | status: 200, 251 | headers: { 'Content-Type': 'application/json' } 252 | }); 253 | } else { 254 | return new Response(JSON.stringify({ 255 | success: false, 256 | message: '找不到域名' 257 | }), { 258 | status: 404, 259 | headers: { 'Content-Type': 'application/json' } 260 | }); 261 | } 262 | } else if (action === 'sync-cloudflare') { 263 | // 同步Cloudflare域名 264 | const cfDomains = await fetchCloudflareDomainsInfo(); 265 | 266 | // 获取域名列表以显示 267 | const domainNamesList = cfDomains.map(d => d.domain); 268 | 269 | // 获取KV中所有域名 270 | const allDomainKeys = await KV_NAMESPACE.list({ prefix: 'whois_' }); 271 | 272 | // 处理KV中的域名 273 | for (const key of allDomainKeys.keys) { 274 | const domainName = key.name.replace('whois_', ''); 275 | const domainData = await getCachedWhoisInfo(domainName); 276 | 277 | // 记录特定域名的信息,用于调试 278 | if (domainName === 'yyas.top') { 279 | console.log('Current yyas.top status:', JSON.stringify(domainData)); 280 | } 281 | 282 | // 如果不是自定义域名,且不在CF域名列表中,则删除 283 | if (domainData && !domainData.isCustom) { 284 | const cfDomain = cfDomains.find(d => d.domain === domainName); 285 | if (!cfDomain) { 286 | console.log(`Removing domain not in CF: ${domainName}`); 287 | await KV_NAMESPACE.delete(key.name); 288 | } 289 | } 290 | } 291 | 292 | // 处理CF中的域名,确保它们在KV中 293 | for (const cfDomain of cfDomains) { 294 | const cachedInfo = await getCachedWhoisInfo(cfDomain.domain); 295 | if (!cachedInfo) { 296 | // 如果是顶级域名,获取WHOIS信息 297 | if (cfDomain.domain.split('.').length === 2 && WHOIS_PROXY_URL) { 298 | try { 299 | const whoisInfo = await fetchWhoisInfo(cfDomain.domain); 300 | await cacheWhoisInfo(cfDomain.domain, { ...cfDomain, ...whoisInfo }); 301 | } catch (error) { 302 | console.error(`Error fetching WHOIS for ${cfDomain.domain}:`, error); 303 | await cacheWhoisInfo(cfDomain.domain, cfDomain); 304 | } 305 | } else { 306 | await cacheWhoisInfo(cfDomain.domain, cfDomain); 307 | } 308 | } 309 | } 310 | 311 | return new Response(JSON.stringify({ 312 | success: true, 313 | message: 'Cloudflare域名同步完成', 314 | count: cfDomains.length, 315 | domains: domainNamesList // 返回域名列表 316 | }), { 317 | status: 200, 318 | headers: { 'Content-Type': 'application/json' } 319 | }); 320 | } else { 321 | // 更新域名信息 322 | let domainInfo = await getCachedWhoisInfo(domain) || {}; 323 | domainInfo = { 324 | ...domainInfo, 325 | registrar, 326 | registrationDate, 327 | expirationDate 328 | }; 329 | await cacheWhoisInfo(domain, domainInfo); 330 | } 331 | 332 | return new Response(JSON.stringify({ success: true }), { 333 | status: 200, 334 | headers: { 'Content-Type': 'application/json' } 335 | }); 336 | } catch (error) { 337 | console.error('Error in handleApiUpdate:', error); 338 | return new Response(JSON.stringify({ success: false, error: error.message }), { 339 | status: 500, 340 | headers: { 'Content-Type': 'application/json' } 341 | }); 342 | } 343 | } 344 | 345 | async function handleWhoisRequest(domain) { 346 | console.log(`Handling WHOIS request for domain: ${domain}`); 347 | 348 | try { 349 | console.log(`Fetching WHOIS data from: ${WHOIS_PROXY_URL}/whois/${domain}`); 350 | const response = await fetch(`${WHOIS_PROXY_URL}/whois/${domain}`); 351 | 352 | if (!response.ok) { 353 | throw new Error(`WHOIS API responded with status: ${response.status}`); 354 | } 355 | 356 | const whoisData = await response.json(); 357 | console.log(`Received WHOIS data:`, whoisData); 358 | 359 | return new Response(JSON.stringify({ 360 | error: false, 361 | rawData: whoisData.rawData 362 | }), { 363 | headers: { 'Content-Type': 'application/json' } 364 | }); 365 | } catch (error) { 366 | console.error(`Error fetching WHOIS data for ${domain}:`, error); 367 | return new Response(JSON.stringify({ 368 | error: true, 369 | message: `Failed to fetch WHOIS data for ${domain}. Error: ${error.message}` 370 | }), { 371 | status: 200, 372 | headers: { 'Content-Type': 'application/json' } 373 | }); 374 | } 375 | } 376 | 377 | async function fetchCloudflareDomainsInfo() { 378 | let allZones = []; 379 | let page = 1; 380 | let hasMorePages = true; 381 | 382 | // 使用分页获取所有域名 383 | while (hasMorePages) { 384 | console.log(`Fetching Cloudflare zones page ${page}...`); 385 | const response = await fetch(`https://api.cloudflare.com/client/v4/zones?page=${page}&per_page=50`, { 386 | headers: { 387 | 'Authorization': `Bearer ${CF_API_KEY}`, 388 | 'Content-Type': 'application/json', 389 | }, 390 | }); 391 | 392 | if (!response.ok) { 393 | throw new Error(`Failed to fetch domains from Cloudflare: ${response.status}`); 394 | } 395 | 396 | const data = await response.json(); 397 | if (!data.success) { 398 | throw new Error('Cloudflare API request failed'); 399 | } 400 | 401 | allZones = [...allZones, ...data.result]; 402 | 403 | // 检查是否有更多页面 404 | if (data.result_info.total_pages > page) { 405 | page++; 406 | } else { 407 | hasMorePages = false; 408 | } 409 | } 410 | 411 | console.log(`Total zones fetched from Cloudflare: ${allZones.length}`); 412 | 413 | // 只返回Zone信息,不获取DNS记录 414 | return allZones.map(zone => ({ 415 | domain: zone.name, 416 | registrationDate: new Date(zone.created_on).toISOString().split('T')[0], 417 | system: 'Cloudflare', 418 | zoneId: zone.id 419 | })); 420 | } 421 | 422 | 423 | async function fetchDomainInfo(domains) { 424 | const result = []; 425 | 426 | // 获取所有域名信息,包括自定义域名 427 | const allDomainKeys = await KV_NAMESPACE.list({ prefix: 'whois_' }); 428 | const allDomains = await Promise.all(allDomainKeys.keys.map(async (key) => { 429 | const value = await KV_NAMESPACE.get(key.name); 430 | if (value) { 431 | try { 432 | const parsedValue = JSON.parse(value); 433 | return parsedValue.data; 434 | } catch (error) { 435 | console.error(`Error parsing data for ${key.name}:`, error); 436 | return null; 437 | } 438 | } 439 | return null; 440 | })); 441 | 442 | // 过滤掉无效的域名数据 443 | const validAllDomains = allDomains.filter(d => d && d.isCustom); 444 | 445 | // 合并 Cloudflare 域名和自定义域名 446 | const mergedDomains = [...domains, ...validAllDomains]; 447 | 448 | for (const domain of mergedDomains) { 449 | if (!domain) continue; // 跳过无效的域名数据 450 | 451 | let domainInfo = { ...domain }; 452 | 453 | const cachedInfo = await getCachedWhoisInfo(domain.domain || domain); 454 | if (cachedInfo) { 455 | domainInfo = { ...domainInfo, ...cachedInfo }; 456 | } else if (!domainInfo.isCustom && domainInfo.domain && domainInfo.domain.split('.').length === 2 && WHOIS_PROXY_URL) { 457 | try { 458 | const whoisInfo = await fetchWhoisInfo(domainInfo.domain); 459 | domainInfo = { ...domainInfo, ...whoisInfo }; 460 | if (!whoisInfo.whoisError) { 461 | await cacheWhoisInfo(domainInfo.domain, whoisInfo); 462 | } 463 | } catch (error) { 464 | console.error(`Error fetching WHOIS info for ${domainInfo.domain}:`, error); 465 | domainInfo.whoisError = error.message; 466 | } 467 | } 468 | 469 | result.push(domainInfo); 470 | } 471 | return result; 472 | } 473 | 474 | async function fetchWhoisInfo(domain) { 475 | try { 476 | console.log(`Fetching WHOIS data for: ${domain}`); 477 | const response = await fetch(`${WHOIS_PROXY_URL}/whois/${domain}`); 478 | 479 | // 检查响应类型 480 | const contentType = response.headers.get('content-type') || ''; 481 | if (!contentType.includes('application/json')) { 482 | console.error(`Received non-JSON response for ${domain}: ${contentType}`); 483 | return { 484 | registrar: 'Unknown', 485 | registrationDate: 'Unknown', 486 | expirationDate: 'Unknown', 487 | whoisError: `服务器返回了非JSON格式 (${contentType})` 488 | }; 489 | } 490 | 491 | // 检查是否为特殊TLD,可能需要特殊处理 492 | const tld = domain.split('.').pop(); 493 | if (tld === 'blog') { 494 | console.log(`Special handling for .${tld} domain`); 495 | } 496 | 497 | const whoisData = await response.json(); 498 | console.log('Raw WHOIS proxy response:', JSON.stringify(whoisData, null, 2)); 499 | 500 | if (whoisData) { 501 | return { 502 | registrar: whoisData.registrar || 'Unknown', 503 | registrationDate: formatDate(whoisData.creationDate) || 'Unknown', 504 | expirationDate: formatDate(whoisData.expirationDate) || 'Unknown' 505 | }; 506 | } else { 507 | console.warn(`Incomplete WHOIS data for ${domain}`); 508 | return { 509 | registrar: 'Unknown', 510 | registrationDate: 'Unknown', 511 | expirationDate: 'Unknown', 512 | whoisError: 'Incomplete WHOIS data' 513 | }; 514 | } 515 | } catch (error) { 516 | console.error(`Error fetching WHOIS info for ${domain}:`, error); 517 | 518 | // 提供更详细的错误信息 519 | let errorMessage = error.message; 520 | if (errorMessage.includes("Unexpected token '<'")) { 521 | errorMessage = "服务器返回了HTML而不是JSON数据。WHOIS服务可能暂时不可用,或者不支持该域名。"; 522 | } 523 | 524 | return { 525 | registrar: 'Unknown', 526 | registrationDate: 'Unknown', 527 | expirationDate: 'Unknown', 528 | whoisError: errorMessage 529 | }; 530 | } 531 | } 532 | 533 | function formatDate(dateString) { 534 | if (!dateString) return null; 535 | const date = new Date(dateString); 536 | return isNaN(date.getTime()) ? dateString : date.toISOString().split('T')[0]; 537 | } 538 | 539 | async function getCachedWhoisInfo(domain) { 540 | const cacheKey = `whois_${domain}`; 541 | const cachedData = await KV_NAMESPACE.get(cacheKey); 542 | if (cachedData) { 543 | try { 544 | const { data, timestamp } = JSON.parse(cachedData); 545 | // 检查是否有错误内容,如果有,删除它 546 | if (data.whoisError) { 547 | await KV_NAMESPACE.delete(cacheKey); 548 | return null; 549 | } 550 | // 这里可以添加缓存过期检查,如果需要的话 551 | return data; 552 | } catch (error) { 553 | console.error(`Error parsing cached data for ${domain}:`, error); 554 | await KV_NAMESPACE.delete(cacheKey); 555 | return null; 556 | } 557 | } 558 | return null; 559 | } 560 | 561 | async function cacheWhoisInfo(domain, whoisInfo) { 562 | const cacheKey = `whois_${domain}`; 563 | await KV_NAMESPACE.put(cacheKey, JSON.stringify({ 564 | data: whoisInfo, 565 | timestamp: Date.now() 566 | })); 567 | } 568 | 569 | function generateLoginHTML(title, action, errorMessage = "") { 570 | return ` 571 | 572 | 573 | 574 | 575 | 576 | ${title} - ${CUSTOM_TITLE} 577 | 689 | 690 | 691 |
692 |

${title}

693 | ${errorMessage ? `

${errorMessage}

` : ''} 694 |
695 | 696 | 697 |
698 |
699 | ${footerHTML} 700 | 701 | 702 | `; 703 | } 704 | 705 | function getStatusColor(daysRemaining) { 706 | if (daysRemaining === 'N/A' || daysRemaining <= 0) return '#e74c3c'; // 红色 707 | if (daysRemaining <= 30) return '#f1c40f'; // 黄色 708 | return '#2ecc71'; // 绿色 709 | } 710 | 711 | function getStatusTitle(daysRemaining) { 712 | if (daysRemaining === 'N/A') return '无效的到期日期'; 713 | if (daysRemaining <= 0) return '已过期'; 714 | if (daysRemaining <= 30) return '即将过期'; 715 | return '正常'; 716 | } 717 | 718 | function generateHTML(domains, isAdmin) { 719 | const categorizedDomains = categorizeDomains(domains); 720 | 721 | console.log("Categorized domains:", categorizedDomains); 722 | const generateTable = (domainList, isCFTopLevel) => { 723 | if (!domainList || !Array.isArray(domainList)) { 724 | console.error('Invalid domainList:', domainList); 725 | return ''; 726 | } 727 | return domainList.map(info => { 728 | const today = new Date(); 729 | const expirationDate = new Date(info.expirationDate); 730 | const daysRemaining = info.expirationDate === 'Unknown' ? 'N/A' : Math.ceil((expirationDate - today) / (1000 * 60 * 60 * 24)); 731 | const totalDays = info.registrationDate === 'Unknown' || info.expirationDate === 'Unknown' ? 'N/A' : Math.ceil((expirationDate - new Date(info.registrationDate)) / (1000 * 60 * 60 * 24)); 732 | const progressPercentage = isNaN(daysRemaining) || isNaN(totalDays) ? 0 : 100 - (daysRemaining / totalDays * 100); 733 | const whoisErrorMessage = info.whoisError 734 | ? `
WHOIS错误: ${info.whoisError}
建议:请检查域名状态或API配置` 735 | : ''; 736 | 737 | let operationButtons = ''; 738 | if (isAdmin) { 739 | if (isCFTopLevel) { 740 | operationButtons = ` 741 | 742 | 743 | 744 | 745 | 746 | ${info.isCustom ? `` : ''} 747 | `; 748 | } else { 749 | operationButtons = ` 750 | 751 | 752 | 753 | ${info.isCustom ? `` : ''} 754 | `; 755 | } 756 | } 757 | 758 | return ` 759 | 760 | 761 | ${info.domain} 762 | ${info.system} 763 | ${info.registrar}${whoisErrorMessage} 764 | ${info.registrationDate} 765 | ${info.expirationDate} 766 | ${daysRemaining} 767 | 768 |
769 |
770 |
771 | 772 | ${isAdmin ? `${operationButtons}` : ''} 773 | 774 | `; 775 | }).join(''); 776 | }; 777 | 778 | const cfTopLevelTable = generateTable(categorizedDomains.cfTopLevel, true); 779 | const cfSecondLevelAndCustomTable = generateTable(categorizedDomains.cfSecondLevelAndCustom, false); 780 | 781 | const adminLink = isAdmin 782 | ? '当前为后台管理页面 | 返回前台' 783 | : '进入后台管理'; 784 | 785 | const adminTools = isAdmin ? ` 786 |
787 | 788 | 789 |
790 | ` : ''; 791 | 792 | return ` 793 | 794 | 795 | 796 | 797 | 798 | ${CUSTOM_TITLE}${isAdmin ? ' - 后台管理' : ''} 799 | 954 | 955 | 956 |
957 |

${CUSTOM_TITLE}${isAdmin ? ' - 后台管理' : ''}

958 | 959 | 960 | ${adminTools} 961 | 962 |
963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | ${isAdmin ? '' : ''} 975 | 976 | 977 | 978 | 979 | ${cfTopLevelTable} 980 | 981 | 982 | ${cfSecondLevelAndCustomTable} 983 | 984 |
状态域名系统注册商注册日期到期日期剩余天数进度操作

CF顶级域名

CF二级域名or自定义域名

985 |
986 | 987 | ${isAdmin ? ` 988 |
989 |

添加CF二级域名or自定义域名

990 |
991 | 992 | 993 | 994 | 995 | 996 | 997 |
998 |
999 | ` : ''} 1000 |
1001 | 1002 | 1003 |
1004 |
1005 | × 1006 |

域名属性

1007 |
1008 |
1009 |
1010 | 1011 | 1316 | ${footerHTML} 1317 | 1318 | 1319 | `; 1320 | } 1321 | 1322 | function getStatusColor(daysRemaining) { 1323 | if (isNaN(daysRemaining)) return '#808080'; // 灰色表示未知状态 1324 | if (daysRemaining <= 7) return '#ff0000'; // 红色 1325 | if (daysRemaining <= 30) return '#ffa500'; // 橙色 1326 | if (daysRemaining <= 90) return '#ffff00'; // 黄色 1327 | return '#00ff00'; // 绿色 1328 | } 1329 | 1330 | function getStatusTitle(daysRemaining) { 1331 | if (isNaN(daysRemaining)) return '未知状态'; 1332 | if (daysRemaining <= 7) return '紧急'; 1333 | if (daysRemaining <= 30) return '警告'; 1334 | if (daysRemaining <= 90) return '注意'; 1335 | return '正常'; 1336 | } 1337 | 1338 | function categorizeDomains(domains) { 1339 | if (!domains || !Array.isArray(domains)) { 1340 | console.error('Invalid domains input:', domains); 1341 | return { cfTopLevel: [], cfSecondLevelAndCustom: [] }; 1342 | } 1343 | 1344 | return domains.reduce((acc, domain) => { 1345 | if (domain.system === 'Cloudflare' && domain.domain.split('.').length === 2) { 1346 | acc.cfTopLevel.push(domain); 1347 | } else { 1348 | acc.cfSecondLevelAndCustom.push(domain); 1349 | } 1350 | return acc; 1351 | }, { cfTopLevel: [], cfSecondLevelAndCustom: [] }); 1352 | } 1353 | --------------------------------------------------------------------------------