实现登录功能

This commit is contained in:
Alex Yang
2025-11-30 11:44:26 +08:00
parent 4f0815a5f9
commit 72aa2846e5
7 changed files with 441 additions and 38 deletions

View File

@@ -1,6 +1,6 @@
{ {
"dns": { "dns": {
"port": 53, "port": 5353,
"upstreamDNS": [ "upstreamDNS": [
"223.5.5.5:53", "223.5.5.5:53",
"223.6.6.6:53" "223.6.6.6:53"
@@ -12,7 +12,9 @@
"http": { "http": {
"port": 8080, "port": 8080,
"host": "0.0.0.0", "host": "0.0.0.0",
"enableAPI": true "enableAPI": true,
"username": "admin",
"password": "admin"
}, },
"shield": { "shield": {
"localRulesFile": "data/rules.txt", "localRulesFile": "data/rules.txt",
@@ -98,7 +100,7 @@
"enabled": true "enabled": true
} }
], ],
"updateInterval": 30, "updateInterval": 3600,
"hostsFile": "data/hosts.txt", "hostsFile": "data/hosts.txt",
"blockMethod": "NXDOMAIN", "blockMethod": "NXDOMAIN",
"customBlockIP": "", "customBlockIP": "",

View File

@@ -19,6 +19,8 @@ type HTTPConfig struct {
Port int `json:"port"` Port int `json:"port"`
Host string `json:"host"` Host string `json:"host"`
EnableAPI bool `json:"enableAPI"` EnableAPI bool `json:"enableAPI"`
Username string `json:"username"` // 登录用户名
Password string `json:"password"` // 登录密码
} }
// BlacklistEntry 黑名单条目 // BlacklistEntry 黑名单条目
@@ -92,6 +94,13 @@ func LoadConfig(path string) (*Config, error) {
if config.HTTP.Host == "" { if config.HTTP.Host == "" {
config.HTTP.Host = "0.0.0.0" config.HTTP.Host = "0.0.0.0"
} }
// 默认用户名和密码如果未配置则使用admin/admin
if config.HTTP.Username == "" {
config.HTTP.Username = "admin"
}
if config.HTTP.Password == "" {
config.HTTP.Password = "admin"
}
if config.Shield.UpdateInterval == 0 { if config.Shield.UpdateInterval == 0 {
config.Shield.UpdateInterval = 3600 config.Shield.UpdateInterval = 3600
} }

View File

@@ -26,6 +26,11 @@ type Server struct {
shieldManager *shield.ShieldManager shieldManager *shield.ShieldManager
server *http.Server server *http.Server
// 会话管理相关字段
sessions map[string]time.Time // 会话ID到过期时间的映射
sessionsMutex sync.Mutex // 会话映射的互斥锁
sessionTTL time.Duration // 会话过期时间
// WebSocket相关字段 // WebSocket相关字段
upgrader websocket.Upgrader upgrader websocket.Upgrader
clients map[*websocket.Conn]bool clients map[*websocket.Conn]bool
@@ -50,10 +55,15 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
}, },
clients: make(map[*websocket.Conn]bool), clients: make(map[*websocket.Conn]bool),
broadcastChan: make(chan []byte, 100), broadcastChan: make(chan []byte, 100),
// 会话管理初始化
sessions: make(map[string]time.Time),
sessionTTL: 24 * time.Hour, // 会话有效期24小时
} }
// 启动广播协程 // 启动广播协程
go server.startBroadcastLoop() go server.startBroadcastLoop()
// 启动会话清理协程
go server.cleanupSessionsLoop()
return server return server
} }
@@ -62,17 +72,26 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
func (s *Server) Start() error { func (s *Server) Start() error {
mux := http.NewServeMux() mux := http.NewServeMux()
// 登录路由,不需要认证
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// 重定向到登录页面HTML
http.Redirect(w, r, "/login.html", http.StatusFound)
})
// API路由 // API路由
if s.config.EnableAPI { if s.config.EnableAPI {
// 重定向/api到Swagger UI页面 // 登录API端点不需要认证
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/login", s.handleLogin)
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
})
// 注册所有API端点 // 重定向/api到Swagger UI页面
mux.HandleFunc("/api/stats", s.handleStats) mux.HandleFunc("/api", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/api/shield", s.handleShield) http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
mux.HandleFunc("/api/shield/localrules", func(w http.ResponseWriter, r *http.Request) { }))
// 注册所有API端点应用登录中间件
mux.HandleFunc("/api/stats", s.loginRequired(s.handleStats))
mux.HandleFunc("/api/shield", s.loginRequired(s.handleShield))
mux.HandleFunc("/api/shield/localrules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
localRules := s.shieldManager.GetLocalRules() localRules := s.shieldManager.GetLocalRules()
@@ -80,8 +99,8 @@ func (s *Server) Start() error {
return return
} }
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}) }))
mux.HandleFunc("/api/shield/remoterules", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/shield/remoterules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
remoteRules := s.shieldManager.GetRemoteRules() remoteRules := s.shieldManager.GetRemoteRules()
@@ -89,45 +108,57 @@ func (s *Server) Start() error {
return return
} }
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}) }))
mux.HandleFunc("/api/shield/hosts", s.handleShieldHosts) mux.HandleFunc("/api/shield/hosts", s.loginRequired(s.handleShieldHosts))
mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists) mux.HandleFunc("/api/shield/blacklists", s.loginRequired(s.handleShieldBlacklists))
mux.HandleFunc("/api/query", s.handleQuery) mux.HandleFunc("/api/query", s.loginRequired(s.handleQuery))
mux.HandleFunc("/api/status", s.handleStatus) mux.HandleFunc("/api/status", s.loginRequired(s.handleStatus))
mux.HandleFunc("/api/config", s.handleConfig) mux.HandleFunc("/api/config", s.loginRequired(s.handleConfig))
mux.HandleFunc("/api/config/restart", s.handleRestart) mux.HandleFunc("/api/config/restart", s.loginRequired(s.handleRestart))
// 添加统计相关接口 // 添加统计相关接口
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains) mux.HandleFunc("/api/top-blocked", s.loginRequired(s.handleTopBlockedDomains))
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains) mux.HandleFunc("/api/top-resolved", s.loginRequired(s.handleTopResolvedDomains))
mux.HandleFunc("/api/top-clients", s.handleTopClients) mux.HandleFunc("/api/top-clients", s.loginRequired(s.handleTopClients))
mux.HandleFunc("/api/top-domains", s.handleTopDomains) mux.HandleFunc("/api/top-domains", s.loginRequired(s.handleTopDomains))
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains) mux.HandleFunc("/api/recent-blocked", s.loginRequired(s.handleRecentBlockedDomains))
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats) mux.HandleFunc("/api/hourly-stats", s.loginRequired(s.handleHourlyStats))
mux.HandleFunc("/api/daily-stats", s.handleDailyStats) mux.HandleFunc("/api/daily-stats", s.loginRequired(s.handleDailyStats))
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats) mux.HandleFunc("/api/monthly-stats", s.loginRequired(s.handleMonthlyStats))
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats) mux.HandleFunc("/api/query/type", s.loginRequired(s.handleQueryTypeStats))
// 日志统计相关接口 // 日志统计相关接口
mux.HandleFunc("/api/logs/stats", s.handleLogsStats) mux.HandleFunc("/api/logs/stats", s.loginRequired(s.handleLogsStats))
mux.HandleFunc("/api/logs/query", s.handleLogsQuery) mux.HandleFunc("/api/logs/query", s.loginRequired(s.handleLogsQuery))
mux.HandleFunc("/api/logs/count", s.handleLogsCount) mux.HandleFunc("/api/logs/count", s.loginRequired(s.handleLogsCount))
// WebSocket端点 // WebSocket端点
mux.HandleFunc("/ws/stats", s.handleWebSocketStats) mux.HandleFunc("/ws/stats", s.loginRequired(s.handleWebSocketStats))
// 将/api/下的静态文件服务指向static/api目录放在最后以避免覆盖API端点 // 将/api/下的静态文件服务指向static/api目录放在最后以避免覆盖API端点
apiFileServer := http.FileServer(http.Dir("./static/api")) apiFileServer := http.FileServer(http.Dir("./static/api"))
mux.Handle("/api/", http.StripPrefix("/api", apiFileServer)) mux.Handle("/api/", s.loginRequired(http.StripPrefix("/api", apiFileServer).ServeHTTP))
} }
// 自定义静态文件服务处理器用于禁用浏览器缓存放在API路由之后 // 自定义静态文件服务处理器用于禁用浏览器缓存放在API路由之后
fileServer := http.FileServer(http.Dir("./static")) fileServer := http.FileServer(http.Dir("./static"))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 单独处理login.html不需要登录
mux.HandleFunc("/login.html", func(w http.ResponseWriter, r *http.Request) {
// 添加Cache-Control头禁用浏览器缓存
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
// 直接提供login.html文件
http.ServeFile(w, r, "./static/login.html")
})
// 其他静态文件需要登录
mux.HandleFunc("/", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
// 添加Cache-Control头禁用浏览器缓存 // 添加Cache-Control头禁用浏览器缓存
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache") w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
// 使用StripPrefix处理路径 // 使用StripPrefix处理路径
http.StripPrefix("/", fileServer).ServeHTTP(w, r) http.StripPrefix("/", fileServer).ServeHTTP(w, r)
}) }))
s.server = &http.Server{ s.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
@@ -378,6 +409,78 @@ func (s *Server) startBroadcastLoop() {
} }
} }
// cleanupSessionsLoop 定期清理过期会话
func (s *Server) cleanupSessionsLoop() {
for {
time.Sleep(1 * time.Hour) // 每小时清理一次
s.sessionsMutex.Lock()
now := time.Now()
for sessionID, expiryTime := range s.sessions {
if now.After(expiryTime) {
delete(s.sessions, sessionID)
}
}
s.sessionsMutex.Unlock()
}
}
// isAuthenticated 检查用户是否已认证
func (s *Server) isAuthenticated(r *http.Request) bool {
// 从Cookie中获取会话ID
cookie, err := r.Cookie("session_id")
if err != nil {
return false
}
sessionID := cookie.Value
s.sessionsMutex.Lock()
defer s.sessionsMutex.Unlock()
// 检查会话是否存在且未过期
expiryTime, exists := s.sessions[sessionID]
if !exists {
return false
}
if time.Now().After(expiryTime) {
// 会话已过期,删除它
delete(s.sessions, sessionID)
return false
}
// 延长会话有效期
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
return true
}
// loginRequired 登录中间件
func (s *Server) loginRequired(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 检查是否为登录页面或登录API允许直接访问
if r.URL.Path == "/login" || r.URL.Path == "/api/login" {
next.ServeHTTP(w, r)
return
}
// 检查是否已认证
if !s.isAuthenticated(r) {
// 如果是API请求返回401错误
if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/ws/") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "未授权访问"})
return
}
// 否则重定向到登录页面
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// 已认证,继续处理请求
next.ServeHTTP(w, r)
}
}
// handleTopBlockedDomains 处理TOP屏蔽域名请求 // handleTopBlockedDomains 处理TOP屏蔽域名请求
func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) { func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
@@ -1312,3 +1415,56 @@ func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"}) json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"})
logger.Info("服务重启成功") logger.Info("服务重启成功")
} }
// handleLogin 处理登录请求
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析请求体
var loginData struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&loginData); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "无效的请求体"})
return
}
// 验证用户名和密码
if loginData.Username != s.config.Username || loginData.Password != s.config.Password {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "用户名或密码错误"})
return
}
// 生成会话ID
sessionID := fmt.Sprintf("%d_%d", time.Now().UnixNano(), len(s.sessions))
// 保存会话
s.sessionsMutex.Lock()
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
s.sessionsMutex.Unlock()
// 设置Cookie
cookie := &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
Expires: time.Now().Add(s.sessionTTL),
HttpOnly: true,
Secure: false, // 开发环境下使用false生产环境应使用true
}
http.SetCookie(w, cookie)
// 返回成功响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "登录成功"})
logger.Info(fmt.Sprintf("用户 %s 登录成功", loginData.Username))
}

View File

@@ -42,9 +42,11 @@ func createDefaultConfig(configFile string) error {
"saveInterval": 300 "saveInterval": 300
}, },
"http": { "http": {
"port": 8081, "port": 8080,
"host": "0.0.0.0", "host": "0.0.0.0",
"enableAPI": true "enableAPI": true,
"username": "admin",
"password": "admin"
}, },
"shield": { "shield": {
"localRulesFile": "data/rules.txt", "localRulesFile": "data/rules.txt",

41
server.log Normal file
View File

@@ -0,0 +1,41 @@
2025/11/30 11:09:05 正在创建所需的文件和文件夹...
2025/11/30 11:09:05 所需文件和文件夹创建成功
time="2025-11-30T11:09:05+08:00" level=debug msg="尝试加载Shield统计数据" file=/root/dnsbak/data/shield_stats.json
time="2025-11-30T11:09:05+08:00" level=info msg="Shield计数数据加载成功" blocked_entries=0 resolved_entries=0
time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt"
time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt"
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt"
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt"
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt"
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt"
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt"
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt"
time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt"
time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt"
time="2025-11-30T11:09:08+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt"
time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt"
time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt"
time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt"
time="2025-11-30T11:09:10+08:00" level=info msg="规则加载完成,域名规则: 189895, 排除规则: 653, 正则规则: 24094, hosts规则: 0"
time="2025-11-30T11:09:10+08:00" level=info msg="统计数据加载成功"
time="2025-11-30T11:09:10+08:00" level=info msg="查询日志加载成功" count=8608
time="2025-11-30T11:09:10+08:00" level=info msg="DNS服务器已启动监听端口: 5353"
time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台已启动监听端口: 8081"
time="2025-11-30T11:09:10+08:00" level=info msg="DNS TCP服务器启动监听端口: 5353"
time="2025-11-30T11:09:10+08:00" level=info msg="启动Shield计数数据自动保存功能" file=./data/shield_stats.json interval=60
time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台服务器启动监听地址: 0.0.0.0:8081"
time="2025-11-30T11:09:10+08:00" level=info msg="规则自动更新已启动" interval=3600
time="2025-11-30T11:09:10+08:00" level=info msg="DNS UDP服务器启动监听端口: 5353"
time="2025-11-30T11:09:10+08:00" level=info msg="启动统计数据自动保存功能" file=data/stats.json interval=300
time="2025-11-30T11:09:10+08:00" level=error msg="DNS UDP服务器启动失败" error="listen udp :5353: bind: address already in use"
time="2025-11-30T11:09:10+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0
2025/11/30 11:09:18 正在关闭服务...
time="2025-11-30T11:09:18+08:00" level=info msg="统计数据保存成功" file=/root/dnsbak/data/stats.json
time="2025-11-30T11:09:18+08:00" level=info msg="查询日志保存成功" file=/root/dnsbak/data/querylog.json
time="2025-11-30T11:09:18+08:00" level=info msg="DNS服务器已停止"
time="2025-11-30T11:09:18+08:00" level=error msg="HTTP控制台服务器启动失败" error="http: Server closed"
time="2025-11-30T11:09:18+08:00" level=info msg="HTTP控制台服务器已停止"
time="2025-11-30T11:09:18+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0
time="2025-11-30T11:09:18+08:00" level=info msg="规则自动更新已停止"
2025/11/30 11:09:18 服务已关闭
time="2025-11-30T11:09:18+08:00" level=warning msg="日志系统已关闭"

View File

@@ -38,6 +38,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
// 优化错误响应处理 // 优化错误响应处理
console.warn(`API请求失败: ${response.status}`); console.warn(`API请求失败: ${response.status}`);
// 处理401未授权错误重定向到登录页面
if (response.status === 401) {
console.warn('未授权访问,重定向到登录页面');
window.location.href = '/login';
return { error: '未授权访问' };
}
// 尝试解析JSON但如果失败直接使用原始文本作为错误信息 // 尝试解析JSON但如果失败直接使用原始文本作为错误信息
try { try {
const errorData = JSON.parse(responseText); const errorData = JSON.parse(responseText);

186
static/login.html Normal file
View File

@@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DNS服务器控制台 - 登录</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: #333;
}
.login-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
font-size: 24px;
color: #2c3e50;
margin-bottom: 8px;
}
.login-header p {
color: #7f8c8d;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #e1e5e9;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
}
.btn {
width: 100%;
padding: 12px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease;
}
.btn:hover {
background-color: #2980b9;
}
.btn:active {
transform: translateY(1px);
}
.error-message {
background-color: #fee;
color: #c00;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
text-align: center;
font-size: 14px;
display: none;
}
.loading {
opacity: 0.7;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>DNS服务器控制台</h1>
<p>请输入您的登录凭据</p>
</div>
<div class="error-message" id="errorMessage"></div>
<form id="loginForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<button type="submit" class="btn" id="loginBtn">登录</button>
</form>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginBtn = document.getElementById('loginBtn');
const errorMessage = document.getElementById('errorMessage');
// 显示加载状态
loginBtn.textContent = '登录中...';
loginBtn.classList.add('loading');
errorMessage.style.display = 'none';
// 发送登录请求
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
})
})
.then(response => {
if (!response.ok) {
throw new Error('登录失败');
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
// 登录成功,重定向到主页
window.location.href = '/';
} else {
throw new Error(data.error || '登录失败');
}
})
.catch(error => {
// 显示错误信息
errorMessage.textContent = error.message;
errorMessage.style.display = 'block';
loginBtn.textContent = '登录';
loginBtn.classList.remove('loading');
});
});
</script>
</body>
</html>