feat: 门户网站初始提交
- Go + Gin + html/template 服务端渲染 - 主页:Google 风格搜索框 + 导航卡片 - 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置 - 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1 - 安全:登录日志、修改密码、IP 自动封禁、IP 白名单 - 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计 - SQLite 存储(modernc.org/sqlite,纯 Go) - 内存 Session + bcrypt 密码哈希
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"simple_portal/database"
|
||||
)
|
||||
|
||||
// 访问动作类型常量
|
||||
const (
|
||||
ActionTypeVisit = "visit" // 主页访问
|
||||
ActionTypeClick = "click" // 卡片点击
|
||||
ActionTypeSearch = "search" // 搜索
|
||||
)
|
||||
|
||||
// AccessLog 表示一条访问日志记录。
|
||||
type AccessLog struct {
|
||||
ID int `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
ActionType string `json:"action_type"` // visit / click / search
|
||||
Detail string `json:"detail"` // 卡片标题 / 搜索关键词
|
||||
Referer string `json:"referer"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// CreateAccessLog 插入一条访问日志。
|
||||
func CreateAccessLog(ip, userAgent, actionType, detail, referer string) error {
|
||||
_, err := database.DB.Exec(
|
||||
"INSERT INTO access_logs (ip, user_agent, action_type, detail, referer, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
ip, userAgent, actionType, detail, referer, time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create access log: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AccessLogStats 访问统计数据
|
||||
type AccessLogStats struct {
|
||||
TodayViews int `json:"today_views"` // 今日浏览次数
|
||||
TotalViews int `json:"total_views"` // 总浏览次数
|
||||
NewIPs int `json:"new_ips"` // 今日新IP数量
|
||||
TotalIPs int `json:"total_ips"` // 总IP数量
|
||||
}
|
||||
|
||||
// GetAccessLogStats 获取访问统计数据
|
||||
func GetAccessLogStats() (*AccessLogStats, error) {
|
||||
stats := &AccessLogStats{}
|
||||
|
||||
// 今日浏览次数
|
||||
err := database.DB.QueryRow(
|
||||
"SELECT COUNT(*) FROM access_logs WHERE date(created_at) = date('now', 'localtime')",
|
||||
).Scan(&stats.TodayViews)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count today views: %w", err)
|
||||
}
|
||||
|
||||
// 总浏览次数
|
||||
err = database.DB.QueryRow("SELECT COUNT(*) FROM access_logs").Scan(&stats.TotalViews)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count total views: %w", err)
|
||||
}
|
||||
|
||||
// 总IP数量
|
||||
err = database.DB.QueryRow("SELECT COUNT(DISTINCT ip) FROM access_logs").Scan(&stats.TotalIPs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count total IPs: %w", err)
|
||||
}
|
||||
|
||||
// 今日新IP数量(今日访问但之前从未出现过的IP)
|
||||
err = database.DB.QueryRow(`
|
||||
SELECT COUNT(DISTINCT a1.ip)
|
||||
FROM access_logs a1
|
||||
WHERE date(a1.created_at) = date('now', 'localtime')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM access_logs a2
|
||||
WHERE a2.ip = a1.ip AND date(a2.created_at) < date('now', 'localtime')
|
||||
)
|
||||
`).Scan(&stats.NewIPs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count new IPs: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetRecentAccessLogs 获取最近的访问日志(用于实时流量展示)
|
||||
func GetRecentAccessLogs(limit int) ([]AccessLog, error) {
|
||||
rows, err := database.DB.Query(
|
||||
"SELECT id, ip, user_agent, action_type, detail, referer, created_at FROM access_logs ORDER BY created_at DESC LIMIT ?",
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query recent access logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []AccessLog
|
||||
for rows.Next() {
|
||||
var l AccessLog
|
||||
if err := rows.Scan(&l.ID, &l.IP, &l.UserAgent, &l.ActionType, &l.Detail, &l.Referer, &l.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan access log: %w", err)
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
return logs, rows.Err()
|
||||
}
|
||||
|
||||
// GetAccessLogs 分页获取访问日志,支持按IP和动作类型筛选。
|
||||
func GetAccessLogs(page, pageSize int, filterIP, filterAction string) ([]AccessLog, int, error) {
|
||||
// 构建查询条件
|
||||
where := "WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
|
||||
if filterIP != "" {
|
||||
where += " AND ip LIKE ?"
|
||||
args = append(args, "%"+filterIP+"%")
|
||||
}
|
||||
if filterAction != "" {
|
||||
where += " AND action_type = ?"
|
||||
args = append(args, filterAction)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
var total int
|
||||
countArgs := make([]interface{}, len(args))
|
||||
copy(countArgs, args)
|
||||
err := database.DB.QueryRow("SELECT COUNT(*) FROM access_logs "+where, countArgs...).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count access logs: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
queryArgs := append(args, pageSize, offset)
|
||||
rows, err := database.DB.Query(
|
||||
"SELECT id, ip, user_agent, action_type, detail, referer, created_at FROM access_logs "+where+" ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
||||
queryArgs...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to query access logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []AccessLog
|
||||
for rows.Next() {
|
||||
var l AccessLog
|
||||
if err := rows.Scan(&l.ID, &l.IP, &l.UserAgent, &l.ActionType, &l.Detail, &l.Referer, &l.CreatedAt); err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to scan access log: %w", err)
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
return logs, total, rows.Err()
|
||||
}
|
||||
|
||||
// GetAccessLogStatsByIP 获取按IP统计的访问数据
|
||||
func GetAccessLogStatsByIP(limit int) ([]struct {
|
||||
IP string `json:"ip"`
|
||||
Visits int `json:"visits"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
}, error) {
|
||||
rows, err := database.DB.Query(
|
||||
"SELECT ip, COUNT(*) as visits, MAX(created_at) as last_seen FROM access_logs GROUP BY ip ORDER BY visits DESC LIMIT ?",
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query IP stats: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []struct {
|
||||
IP string `json:"ip"`
|
||||
Visits int `json:"visits"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
}
|
||||
for rows.Next() {
|
||||
var r struct {
|
||||
IP string `json:"ip"`
|
||||
Visits int `json:"visits"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
}
|
||||
if err := rows.Scan(&r.IP, &r.Visits, &r.LastSeen); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan IP stats: %w", err)
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"simple_portal/database"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Admin represents an administrator account.
|
||||
type Admin struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"` // Never expose password hash in JSON
|
||||
}
|
||||
|
||||
// GetAdminByUsername returns an admin by username. Returns nil if not found.
|
||||
func GetAdminByUsername(username string) (*Admin, error) {
|
||||
var a Admin
|
||||
err := database.DB.QueryRow(
|
||||
"SELECT id, username, password FROM admins WHERE username = ?",
|
||||
username,
|
||||
).Scan(&a.ID, &a.Username, &a.Password)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get admin by username: %w", err)
|
||||
}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// CreateAdmin inserts a new admin with a bcrypt-hashed password.
|
||||
func CreateAdmin(username, hashedPassword string) error {
|
||||
_, err := database.DB.Exec(
|
||||
"INSERT INTO admins (username, password) VALUES (?, ?)",
|
||||
username, hashedPassword,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create admin: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyPassword checks if the given plain-text password matches the stored bcrypt hash.
|
||||
// Returns (matched, admin, error).
|
||||
func VerifyPassword(username, password string) (bool, *Admin, error) {
|
||||
admin, err := GetAdminByUsername(username)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if admin == nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password))
|
||||
if err != nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
return true, admin, nil
|
||||
}
|
||||
|
||||
// ChangePassword updates the password hash for an admin by ID.
|
||||
func ChangePassword(adminID int, newHashedPassword string) error {
|
||||
_, err := database.DB.Exec(
|
||||
"UPDATE admins SET password = ? WHERE id = ?",
|
||||
newHashedPassword, adminID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to change password: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"simple_portal/database"
|
||||
)
|
||||
|
||||
// Card represents a navigation card on the portal home page.
|
||||
type Card struct {
|
||||
ID int `json:"id"`
|
||||
Icon string `json:"icon"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
URL string `json:"url"`
|
||||
Sort int `json:"sort"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// GetAllCards returns all cards ordered by sort ascending.
|
||||
func GetAllCards() ([]Card, error) {
|
||||
rows, err := database.DB.Query("SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards ORDER BY sort ASC")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query cards: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var cards []Card
|
||||
for rows.Next() {
|
||||
var c Card
|
||||
var enabled int
|
||||
if err := rows.Scan(&c.ID, &c.Icon, &c.Title, &c.Subtitle, &c.URL, &c.Sort, &enabled, &c.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan card: %w", err)
|
||||
}
|
||||
c.Enabled = enabled == 1
|
||||
cards = append(cards, c)
|
||||
}
|
||||
return cards, rows.Err()
|
||||
}
|
||||
|
||||
// GetEnabledCards returns only enabled cards ordered by sort ascending.
|
||||
func GetEnabledCards() ([]Card, error) {
|
||||
rows, err := database.DB.Query("SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE enabled = 1 ORDER BY sort ASC")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query enabled cards: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var cards []Card
|
||||
for rows.Next() {
|
||||
var c Card
|
||||
var enabled int
|
||||
if err := rows.Scan(&c.ID, &c.Icon, &c.Title, &c.Subtitle, &c.URL, &c.Sort, &enabled, &c.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan card: %w", err)
|
||||
}
|
||||
c.Enabled = enabled == 1
|
||||
cards = append(cards, c)
|
||||
}
|
||||
return cards, rows.Err()
|
||||
}
|
||||
|
||||
// GetCardByID returns a single card by its ID.
|
||||
func GetCardByID(id int) (*Card, error) {
|
||||
var c Card
|
||||
var enabled int
|
||||
err := database.DB.QueryRow(
|
||||
"SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE id = ?",
|
||||
id,
|
||||
).Scan(&c.ID, &c.Icon, &c.Title, &c.Subtitle, &c.URL, &c.Sort, &enabled, &c.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query card by id: %w", err)
|
||||
}
|
||||
c.Enabled = enabled == 1
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// CreateCard inserts a new card with sort = MAX(sort) + 1.
|
||||
func CreateCard(card *Card) error {
|
||||
var maxSort sql.NullInt64
|
||||
err := database.DB.QueryRow("SELECT MAX(sort) FROM cards").Scan(&maxSort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get max sort: %w", err)
|
||||
}
|
||||
|
||||
newSort := 0
|
||||
if maxSort.Valid {
|
||||
newSort = int(maxSort.Int64) + 1
|
||||
}
|
||||
|
||||
enabledInt := 0
|
||||
if card.Enabled {
|
||||
enabledInt = 1
|
||||
}
|
||||
|
||||
result, err := database.DB.Exec(
|
||||
"INSERT INTO cards (icon, title, subtitle, url, sort, enabled) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
card.Icon, card.Title, card.Subtitle, card.URL, newSort, enabledInt,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert card: %w", err)
|
||||
}
|
||||
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get last insert id: %w", err)
|
||||
}
|
||||
card.ID = int(lastID)
|
||||
card.Sort = newSort
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCard updates an existing card by ID.
|
||||
func UpdateCard(card *Card) error {
|
||||
enabledInt := 0
|
||||
if card.Enabled {
|
||||
enabledInt = 1
|
||||
}
|
||||
|
||||
_, err := database.DB.Exec(
|
||||
"UPDATE cards SET icon = ?, title = ?, subtitle = ?, url = ?, sort = ?, enabled = ? WHERE id = ?",
|
||||
card.Icon, card.Title, card.Subtitle, card.URL, card.Sort, enabledInt, card.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update card: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCard deletes a card by ID.
|
||||
func DeleteCard(id int) error {
|
||||
_, err := database.DB.Exec("DELETE FROM cards WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete card: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleCard toggles the enabled status of a card.
|
||||
func ToggleCard(id int) error {
|
||||
_, err := database.DB.Exec("UPDATE cards SET enabled = CASE WHEN enabled = 1 THEN 0 ELSE 1 END WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to toggle card: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveCardUp swaps the sort value of the card with the one above it.
|
||||
func MoveCardUp(id int) error {
|
||||
// Get the current card
|
||||
card, err := GetCardByID(id)
|
||||
if err != nil || card == nil {
|
||||
return fmt.Errorf("card not found: %d", id)
|
||||
}
|
||||
|
||||
// Find the card with the next lower sort value (the one above)
|
||||
var aboveCard Card
|
||||
var aboveEnabled int
|
||||
err = database.DB.QueryRow(
|
||||
"SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE sort < ? ORDER BY sort DESC LIMIT 1",
|
||||
card.Sort,
|
||||
).Scan(&aboveCard.ID, &aboveCard.Icon, &aboveCard.Title, &aboveCard.Subtitle, &aboveCard.URL, &aboveCard.Sort, &aboveEnabled, &aboveCard.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
// Already at the top
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find card above: %w", err)
|
||||
}
|
||||
aboveCard.Enabled = aboveEnabled == 1
|
||||
|
||||
// Swap sort values
|
||||
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", aboveCard.Sort, card.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to swap sort (current): %w", err)
|
||||
}
|
||||
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", card.Sort, aboveCard.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to swap sort (above): %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveCardDown swaps the sort value of the card with the one below it.
|
||||
func MoveCardDown(id int) error {
|
||||
// Get the current card
|
||||
card, err := GetCardByID(id)
|
||||
if err != nil || card == nil {
|
||||
return fmt.Errorf("card not found: %d", id)
|
||||
}
|
||||
|
||||
// Find the card with the next higher sort value (the one below)
|
||||
var belowCard Card
|
||||
var belowEnabled int
|
||||
err = database.DB.QueryRow(
|
||||
"SELECT id, icon, title, subtitle, url, sort, enabled, created_at FROM cards WHERE sort > ? ORDER BY sort ASC LIMIT 1",
|
||||
card.Sort,
|
||||
).Scan(&belowCard.ID, &belowCard.Icon, &belowCard.Title, &belowCard.Subtitle, &belowCard.URL, &belowCard.Sort, &belowEnabled, &belowCard.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
// Already at the bottom
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find card below: %w", err)
|
||||
}
|
||||
belowCard.Enabled = belowEnabled == 1
|
||||
|
||||
// Swap sort values
|
||||
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", belowCard.Sort, card.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to swap sort (current): %w", err)
|
||||
}
|
||||
_, err = database.DB.Exec("UPDATE cards SET sort = ? WHERE id = ?", card.Sort, belowCard.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to swap sort (below): %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"simple_portal/database"
|
||||
)
|
||||
|
||||
// IPBan 表示一条IP封禁记录。
|
||||
type IPBan struct {
|
||||
ID int `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Reason string `json:"reason"`
|
||||
FailCount int `json:"fail_count"`
|
||||
BannedUntil time.Time `json:"banned_until"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// IsIPBanned 检查指定IP是否处于封禁状态(未过期)。
|
||||
// 白名单IP不会被检查,需要在调用前自行判断。
|
||||
func IsIPBanned(ip string) (bool, *IPBan, error) {
|
||||
var ban IPBan
|
||||
err := database.DB.QueryRow(
|
||||
"SELECT id, ip, reason, fail_count, banned_until, created_at FROM ip_bans WHERE ip = ? AND banned_until > ? ORDER BY banned_until DESC LIMIT 1",
|
||||
ip, time.Now().Format("2006-01-02 15:04:05"),
|
||||
).Scan(&ban.ID, &ban.IP, &ban.Reason, &ban.FailCount, &ban.BannedUntil, &ban.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to check IP ban: %w", err)
|
||||
}
|
||||
return true, &ban, nil
|
||||
}
|
||||
|
||||
// CreateIPBan 插入一条IP封禁记录。
|
||||
func CreateIPBan(ip, reason string, failCount int, bannedUntil time.Time) error {
|
||||
_, err := database.DB.Exec(
|
||||
"INSERT INTO ip_bans (ip, reason, fail_count, banned_until, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
ip, reason, failCount, bannedUntil.Format("2006-01-02 15:04:05"), time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IP ban: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllActiveBans 获取所有未过期的封禁记录。
|
||||
func GetAllActiveBans() ([]IPBan, error) {
|
||||
rows, err := database.DB.Query(
|
||||
"SELECT id, ip, reason, fail_count, banned_until, created_at FROM ip_bans WHERE banned_until > ? ORDER BY banned_until DESC",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query active bans: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var bans []IPBan
|
||||
for rows.Next() {
|
||||
var b IPBan
|
||||
if err := rows.Scan(&b.ID, &b.IP, &b.Reason, &b.FailCount, &b.BannedUntil, &b.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan IP ban: %w", err)
|
||||
}
|
||||
bans = append(bans, b)
|
||||
}
|
||||
return bans, nil
|
||||
}
|
||||
|
||||
// DeleteIPBan 删除一条封禁记录(手动解封)。
|
||||
func DeleteIPBan(id int) error {
|
||||
_, err := database.DB.Exec("DELETE FROM ip_bans WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete IP ban: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"simple_portal/database"
|
||||
)
|
||||
|
||||
// IPWhitelist 表示一条IP白名单记录。
|
||||
type IPWhitelist struct {
|
||||
ID int `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// HasWhitelist 检查是否存在白名单记录。
|
||||
// 如果没有白名单记录,则不限制任何IP。
|
||||
func HasWhitelist() (bool, error) {
|
||||
var count int
|
||||
err := database.DB.QueryRow("SELECT COUNT(*) FROM ip_whitelist").Scan(&count)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check whitelist: %w", err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// IsIPWhitelisted 检查IP是否在白名单中。
|
||||
func IsIPWhitelisted(ip string) (bool, error) {
|
||||
var count int
|
||||
err := database.DB.QueryRow("SELECT COUNT(*) FROM ip_whitelist WHERE ip = ?", ip).Scan(&count)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check IP whitelist: %w", err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetAllWhitelist 获取所有白名单记录。
|
||||
func GetAllWhitelist() ([]IPWhitelist, error) {
|
||||
rows, err := database.DB.Query("SELECT id, ip, comment, created_at FROM ip_whitelist ORDER BY created_at DESC")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query whitelist: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var list []IPWhitelist
|
||||
for rows.Next() {
|
||||
var w IPWhitelist
|
||||
if err := rows.Scan(&w.ID, &w.IP, &w.Comment, &w.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan whitelist: %w", err)
|
||||
}
|
||||
list = append(list, w)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// AddWhitelist 添加一条白名单记录。
|
||||
func AddWhitelist(ip, comment string) error {
|
||||
_, err := database.DB.Exec(
|
||||
"INSERT INTO ip_whitelist (ip, comment, created_at) VALUES (?, ?, ?)",
|
||||
ip, comment, time.Now(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add whitelist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWhitelist 删除一条白名单记录。
|
||||
func DeleteWhitelist(id int) error {
|
||||
_, err := database.DB.Exec("DELETE FROM ip_whitelist WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete whitelist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"simple_portal/database"
|
||||
)
|
||||
|
||||
// LoginLog 表示一条登录日志记录。
|
||||
type LoginLog struct {
|
||||
ID int `json:"id"`
|
||||
AdminID *int `json:"admin_id"`
|
||||
Username string `json:"username"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Success bool `json:"success"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// CreateLoginLog 插入一条登录日志。
|
||||
func CreateLoginLog(adminID *int, username, ip, userAgent string, success bool) error {
|
||||
_, err := database.DB.Exec(
|
||||
"INSERT INTO login_logs (admin_id, username, ip, user_agent, success, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
adminID, username, ip, userAgent, success, time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create login log: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoginLogCount 返回登录日志总数。
|
||||
func LoginLogCount() (int, error) {
|
||||
var count int
|
||||
err := database.DB.QueryRow("SELECT COUNT(*) FROM login_logs").Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to count login logs: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetLoginLogs 分页获取登录日志,倒序排列。
|
||||
func GetLoginLogs(page, pageSize int) ([]LoginLog, int, error) {
|
||||
total, err := LoginLogCount()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
rows, err := database.DB.Query(
|
||||
"SELECT id, admin_id, username, ip, user_agent, success, created_at FROM login_logs ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
||||
pageSize, offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to query login logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []LoginLog
|
||||
for rows.Next() {
|
||||
var l LoginLog
|
||||
if err := rows.Scan(&l.ID, &l.AdminID, &l.Username, &l.IP, &l.UserAgent, &l.Success, &l.CreatedAt); err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to scan login log: %w", err)
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
return logs, total, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"simple_portal/database"
|
||||
)
|
||||
|
||||
// Predefined search engine URL templates.
|
||||
const (
|
||||
SearchEngineGoogle = "https://www.google.com/search?q=%s"
|
||||
SearchEngineBing = "https://www.bing.com/search?q=%s"
|
||||
SearchEngineBaidu = "https://www.baidu.com/s?wd=%s"
|
||||
)
|
||||
|
||||
// SettingKey constants.
|
||||
const (
|
||||
SettingKeySearchEngine = "search_engine"
|
||||
SettingKeyHomepageTitle = "homepage_title"
|
||||
SettingKeyHomepageSubtitle = "homepage_subtitle"
|
||||
SettingKeyHomepageBackground = "homepage_background"
|
||||
)
|
||||
|
||||
// Default setting values.
|
||||
const (
|
||||
DefaultHomepageTitle = "Portal"
|
||||
DefaultHomepageSubtitle = ""
|
||||
DefaultHomepageBackground = ""
|
||||
)
|
||||
|
||||
// GetSetting retrieves a setting value by key. Returns empty string if not found.
|
||||
func GetSetting(key string) (string, error) {
|
||||
var value sql.NullString
|
||||
err := database.DB.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get setting %s: %w", key, err)
|
||||
}
|
||||
return value.String, nil
|
||||
}
|
||||
|
||||
// SetSetting inserts or updates a setting value by key.
|
||||
func SetSetting(key, value string) error {
|
||||
_, err := database.DB.Exec(
|
||||
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",
|
||||
key, value, value,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set setting %s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user