mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat: add IP limit live integration - IP-based access control with database tracking
This commit is contained in:
parent
3f0b7fbe97
commit
d1de02eabf
7 changed files with 768 additions and 0 deletions
31
database/db_migration_ip_limit.go
Normal file
31
database/db_migration_ip_limit.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MigrateIPLimit creates the inbound_client_ips table
|
||||
func MigrateIPLimit(db *gorm.DB) error {
|
||||
log.Println("[DB] Migrating IP limit table...")
|
||||
|
||||
type InboundClientIPs struct {
|
||||
Id int `gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `gorm:"unique"`
|
||||
IPs string `gorm:"type:text"`
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
if !db.Migrator().HasTable(&InboundClientIPs{}) {
|
||||
if err := db.Migrator().CreateTable(&InboundClientIPs{}); err != nil {
|
||||
return fmt.Errorf("failed to create inbound_client_ips table: %w", err)
|
||||
}
|
||||
log.Println("[DB] Created inbound_client_ips table")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
19
database/model/ip_limit_model.go
Normal file
19
database/model/ip_limit_model.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// InboundClientIPs stores IP information for clients with IP-based access control
|
||||
type InboundClientIPs struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||
IPs string `json:"ips" form:"ips" gorm:"type:text"` // Comma-separated IP list
|
||||
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for InboundClientIPs
|
||||
func (InboundClientIPs) TableName() string {
|
||||
return "inbound_client_ips"
|
||||
}
|
||||
311
docs/IP_LIMIT_INTEGRATION_GUIDE.md
Normal file
311
docs/IP_LIMIT_INTEGRATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
# IP Limit Live Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The IP Limit feature provides IP-based access control. Clients can connect from multiple IPs up to a configured limit while maintaining security.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE inbound_client_ips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_email TEXT UNIQUE NOT NULL,
|
||||
ips TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER DEFAULT 0,
|
||||
updated_at INTEGER DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
### IP Storage Format
|
||||
|
||||
IPs are stored as comma-separated values:
|
||||
```
|
||||
client_email: user@example.com
|
||||
ips: 192.168.1.100,203.0.113.45,198.51.100.200
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. IPLimitService (`web/service/ip_limit_service.go`)
|
||||
|
||||
Core IP limit functionality:
|
||||
|
||||
- **CheckIPLimit(email, limit, newIP)** - Validates if a new IP exceeds the limit
|
||||
- **RecordIPAccess(email, ip)** - Records IP access
|
||||
- **GetClientIPs(email)** - Retrieves all IPs for a client
|
||||
- **RemoveIP(email, ipToRemove)** - Removes a specific IP
|
||||
- **ClearAllIPs(email)** - Clears all IPs for a client
|
||||
|
||||
### 2. Database Model (`database/model/ip_limit_model.go`)
|
||||
|
||||
Defines the `InboundClientIPs` model.
|
||||
|
||||
### 3. API Endpoints (`web/controller/ip_limit_api.go`)
|
||||
|
||||
#### GET `/api/client/ips/:email`
|
||||
Returns all IPs for a client.
|
||||
|
||||
```bash
|
||||
curl http://localhost:2053/api/client/ips/user@example.com
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"msg": "success",
|
||||
"ips": [
|
||||
"192.168.1.100",
|
||||
"203.0.113.45"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### DELETE `/api/client/ips/:email/:ip`
|
||||
Removes a specific IP.
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:2053/api/client/ips/user@example.com/192.168.1.100
|
||||
```
|
||||
|
||||
#### DELETE `/api/client/ips/:email`
|
||||
Clears all IPs for a client.
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:2053/api/client/ips/user@example.com
|
||||
```
|
||||
|
||||
### 4. Access Service (`web/service/inbound_client_access_service.go`)
|
||||
|
||||
Integrated access control:
|
||||
|
||||
- **CheckClientAccess(email, ip, limitIP)** - Live IP validation and logging
|
||||
- **ValidateClientIP(email, ip, limitIP)** - Pure validation
|
||||
- **GetClientIPList(email)** - Get all registered IPs
|
||||
- **RemoveClientIP(email, ip)** - Remove specific IP
|
||||
|
||||
## Usage Example
|
||||
|
||||
### In Login Handler
|
||||
|
||||
```go
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/service"
|
||||
)
|
||||
|
||||
func (a *APIController) Login(ctx *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.AbortWithStatusJSON(400, gin.H{"msg": "Invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate credentials
|
||||
client, err := a.getClientByEmail(req.Email)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(401, gin.H{"msg": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get client IP
|
||||
clientIP := ctx.ClientIP()
|
||||
|
||||
// Create inbound service
|
||||
inboundSvc := &service.InboundService{}
|
||||
|
||||
// Check IP limit (LIVE INTEGRATION)
|
||||
allowed, message, err := inboundSvc.CheckClientAccess(
|
||||
client.Email,
|
||||
clientIP,
|
||||
client.LimitIP, // IP limit from client config
|
||||
)
|
||||
|
||||
if !allowed {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{
|
||||
"msg": message,
|
||||
"error": "ip_limit_exceeded",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Issue login token
|
||||
token := generateToken(client)
|
||||
ctx.JSON(200, gin.H{
|
||||
"msg": "login_success",
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### In Middleware
|
||||
|
||||
```go
|
||||
func IPLimitMiddleware() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
clientEmail := ctx.GetString("client_email")
|
||||
clientIP := ctx.ClientIP()
|
||||
limitIP := ctx.GetInt("limit_ip")
|
||||
|
||||
inboundSvc := &service.InboundService{}
|
||||
allowed, _, err := inboundSvc.CheckClientAccess(
|
||||
clientEmail,
|
||||
clientIP,
|
||||
limitIP,
|
||||
)
|
||||
|
||||
if !allowed || err != nil {
|
||||
ctx.AbortWithStatusJSON(403, gin.H{
|
||||
"msg": "Access denied",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Client Model Field
|
||||
|
||||
In `database/model/client.go`, add:
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
// ... other fields ...
|
||||
LimitIP int `json:"limitIP"` // Number of allowed IPs (0 = unlimited)
|
||||
}
|
||||
```
|
||||
|
||||
## API Routes Registration
|
||||
|
||||
Register these routes in your router setup:
|
||||
|
||||
```go
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/controller"
|
||||
)
|
||||
|
||||
func SetupRoutes(r *gin.Engine, apiController *controller.APIController) {
|
||||
api := r.Group("/api")
|
||||
{
|
||||
client := api.Group("/client")
|
||||
{
|
||||
// IP management endpoints
|
||||
client.GET("/ips/:email", apiController.GetClientIPs)
|
||||
client.DELETE("/ips/:email/:ip", apiController.ClearClientIP)
|
||||
client.DELETE("/ips/:email", apiController.ClearAllClientIPs)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Database Migration
|
||||
|
||||
Call migration in your startup code:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mhsanaei/3x-ui/v3/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db := database.GetDB()
|
||||
database.MigrateIPLimit(db)
|
||||
}
|
||||
```
|
||||
|
||||
## Live Integration Workflow
|
||||
|
||||
```
|
||||
Client Login Request
|
||||
↓
|
||||
[Extract] Client IP from request
|
||||
↓
|
||||
[Validate] Credentials
|
||||
↓
|
||||
[Check] IP Limit via CheckClientAccess()
|
||||
├─ Database lookup for existing IPs
|
||||
├─ Count unique IPs from last 30 days
|
||||
├─ Compare with limit threshold
|
||||
└─ If exceeded: Return 403 error
|
||||
↓
|
||||
[Record] IP access in database
|
||||
↓
|
||||
[Issue] Login token
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Get Client IPs
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:2053/api/client/ips/user@example.com
|
||||
```
|
||||
|
||||
### Remove Specific IP
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:2053/api/client/ips/user@example.com/192.168.1.100
|
||||
```
|
||||
|
||||
### Clear All IPs
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:2053/api/client/ips/user@example.com
|
||||
```
|
||||
|
||||
### Login Test with IP Check
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:2053/api/client/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "user@example.com",
|
||||
"secret": "client-secret"
|
||||
}'
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **IP Spoofing Prevention** - Use `X-Forwarded-For` header carefully in production
|
||||
2. **Stale IP Cleanup** - Automatically clean up old IPs after 30 days
|
||||
3. **Rate Limiting** - Implement rate limiting on IP registration
|
||||
4. **Logging** - Log all IP registration and access attempts
|
||||
5. **Load Balancer** - Ensure consistent IP detection behind load balancers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### IP Not Being Recorded
|
||||
- Check if `RecordIPAccess()` is being called
|
||||
- Verify database permissions
|
||||
- Check logs for SQL errors
|
||||
|
||||
### Limit Not Enforced
|
||||
- Verify `LimitIP` value in client configuration
|
||||
- Check if `CheckIPLimit()` is called before granting access
|
||||
- Ensure database migration ran successfully
|
||||
|
||||
### Wrong IP Detected
|
||||
- Check client IP extraction logic
|
||||
- Verify load balancer configuration
|
||||
- Check `X-Forwarded-For` header handling
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- IP geolocation tracking
|
||||
- VPN/Proxy detection
|
||||
- IP reputation scoring
|
||||
- Automatic IP whitelisting
|
||||
- IP-based access policies
|
||||
- IP anomaly detection
|
||||
72
web/controller/ip_limit_api.go
Normal file
72
web/controller/ip_limit_api.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mhsanaei/3x-ui/v3/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/service"
|
||||
)
|
||||
|
||||
// GetClientIPs returns all IPs for a client email
|
||||
func (a *APIController) GetClientIPs(ctx *gin.Context) {
|
||||
email := ctx.Param("email")
|
||||
if email == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"msg": "email is required"})
|
||||
return
|
||||
}
|
||||
|
||||
ipSvc := &service.IPLimitService{}
|
||||
ips, err := ipSvc.GetClientIPs(email)
|
||||
if err != nil {
|
||||
logger.Error("GetClientIPs error:", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"msg": "success",
|
||||
"ips": ips,
|
||||
})
|
||||
}
|
||||
|
||||
// ClearClientIP removes a specific IP
|
||||
func (a *APIController) ClearClientIP(ctx *gin.Context) {
|
||||
email := ctx.Param("email")
|
||||
ip := ctx.Param("ip")
|
||||
|
||||
if email == "" || ip == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"msg": "email and ip are required"})
|
||||
return
|
||||
}
|
||||
|
||||
ipSvc := &service.IPLimitService{}
|
||||
err := ipSvc.RemoveIP(email, ip)
|
||||
if err != nil {
|
||||
logger.Error("ClearClientIP error:", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"msg": "ip removed"})
|
||||
}
|
||||
|
||||
// ClearAllClientIPs removes all IPs for a client
|
||||
func (a *APIController) ClearAllClientIPs(ctx *gin.Context) {
|
||||
email := ctx.Param("email")
|
||||
if email == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"msg": "email is required"})
|
||||
return
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
if err := db.Where("client_email = ?", email).Delete(&model.InboundClientIPs{}).Error; err != nil {
|
||||
logger.Error("ClearAllClientIPs error:", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"msg": "all ips cleared"})
|
||||
}
|
||||
94
web/html/src/utils/ipLimitUtils.js
Normal file
94
web/html/src/utils/ipLimitUtils.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* IP Limit Integration Utilities
|
||||
* Helper functions for IP-based access control on frontend
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get client IP from backend response
|
||||
* Used after successful authentication
|
||||
*/
|
||||
export function getClientIPInfo() {
|
||||
return {
|
||||
ip: window.location.hostname || 'unknown',
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display IP limit error message
|
||||
*/
|
||||
export function displayIPLimitError(message) {
|
||||
const errorMsg = message || 'IP limit exceeded. Please contact administrator.'
|
||||
return {
|
||||
error: true,
|
||||
message: errorMsg,
|
||||
type: 'ip_limit_exceeded',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and display all client IPs
|
||||
*/
|
||||
export async function getClientIPs(clientEmail) {
|
||||
try {
|
||||
const response = await fetch(`/api/client/ips/${clientEmail}`)
|
||||
const data = await response.json()
|
||||
return data.ips || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch client IPs:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific IP from client
|
||||
*/
|
||||
export async function removeClientIP(clientEmail, ip) {
|
||||
try {
|
||||
const response = await fetch(`/api/client/ips/${clientEmail}/${ip}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
return response.ok
|
||||
} catch (error) {
|
||||
console.error('Failed to remove IP:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all client IPs
|
||||
*/
|
||||
export async function clearAllClientIPs(clientEmail) {
|
||||
try {
|
||||
const response = await fetch(`/api/client/ips/${clientEmail}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
return response.ok
|
||||
} catch (error) {
|
||||
console.error('Failed to clear all IPs:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format IP for display
|
||||
*/
|
||||
export function formatIPAddress(ip) {
|
||||
return ip || 'N/A'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP is IPv4
|
||||
*/
|
||||
export function isIPv4(ip) {
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/
|
||||
return ipv4Regex.test(ip)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP is IPv6
|
||||
*/
|
||||
export function isIPv6(ip) {
|
||||
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4})$/
|
||||
return ipv6Regex.test(ip)
|
||||
}
|
||||
57
web/service/inbound_client_access_service.go
Normal file
57
web/service/inbound_client_access_service.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
)
|
||||
|
||||
// CheckClientAccess validates if a client can access based on IP limit
|
||||
// Combines IP validation and access logging
|
||||
func (s *InboundService) CheckClientAccess(clientEmail string, clientIP string, limitIP int) (bool, string, error) {
|
||||
if limitIP <= 0 {
|
||||
return true, "Access allowed (no limit)", nil
|
||||
}
|
||||
|
||||
// Check IP limit
|
||||
ipSvc := &IPLimitService{}
|
||||
ipAllowed, err := ipSvc.CheckIPLimit(clientEmail, limitIP, clientIP)
|
||||
if err != nil {
|
||||
logger.Error("[IPLimit] Check error for", clientEmail, err)
|
||||
return false, "IP limit check error", err
|
||||
}
|
||||
|
||||
if !ipAllowed {
|
||||
msg := fmt.Sprintf("IP limit exceeded (max %d IPs allowed)", limitIP)
|
||||
logger.Warn("[IPLimit] Limit exceeded for", clientEmail, "IP:", clientIP)
|
||||
return false, msg, nil
|
||||
}
|
||||
|
||||
// Record IP access
|
||||
err = ipSvc.RecordIPAccess(clientEmail, clientIP)
|
||||
if err != nil {
|
||||
logger.Error("[IPLimit] Failed to record IP access:", err)
|
||||
return false, "Failed to record IP access", err
|
||||
}
|
||||
|
||||
logger.Info("[IPLimit] Access allowed for", clientEmail, "IP:", clientIP)
|
||||
return true, "Access allowed", nil
|
||||
}
|
||||
|
||||
// ValidateClientIP performs comprehensive IP validation
|
||||
func (s *InboundService) ValidateClientIP(clientEmail string, clientIP string, limitIP int) (bool, error) {
|
||||
ipSvc := &IPLimitService{}
|
||||
return ipSvc.CheckIPLimit(clientEmail, limitIP, clientIP)
|
||||
}
|
||||
|
||||
// GetClientIPList retrieves all registered IPs for a client
|
||||
func (s *InboundService) GetClientIPList(clientEmail string) ([]string, error) {
|
||||
ipSvc := &IPLimitService{}
|
||||
return ipSvc.GetClientIPs(clientEmail)
|
||||
}
|
||||
|
||||
// RemoveClientIP removes a specific IP from client's registry
|
||||
func (s *InboundService) RemoveClientIP(clientEmail string, ipToRemove string) error {
|
||||
ipSvc := &IPLimitService{}
|
||||
return ipSvc.RemoveIP(clientEmail, ipToRemove)
|
||||
}
|
||||
184
web/service/ip_limit_service.go
Normal file
184
web/service/ip_limit_service.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IPLimitService struct{}
|
||||
|
||||
// IPRecord stores client IP information
|
||||
type IPRecord struct {
|
||||
IP string `json:"ip"`
|
||||
LastSeen int64 `json:"lastSeen"`
|
||||
FirstSeen int64 `json:"firstSeen"`
|
||||
}
|
||||
|
||||
const (
|
||||
// IP stale cutoff: 30 days
|
||||
IPStaleCutoffDays = 30
|
||||
)
|
||||
|
||||
// CheckIPLimit validates if a client has exceeded IP limit
|
||||
// Returns (allowed, error)
|
||||
func (s *IPLimitService) CheckIPLimit(clientEmail string, limit int, newIP string) (bool, error) {
|
||||
if limit <= 0 {
|
||||
return true, nil // No limit
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
var record model.InboundClientIPs
|
||||
|
||||
err := db.Where("client_email = ?", clientEmail).First(&record).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return true, nil // No IPs recorded yet
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Parse IP list
|
||||
ipList := strings.Split(record.IPs, ",")
|
||||
var cleanIPs []string
|
||||
for _, ip := range ipList {
|
||||
if trimmed := strings.TrimSpace(ip); trimmed != "" {
|
||||
cleanIPs = append(cleanIPs, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if new IP exists
|
||||
ipExists := false
|
||||
for _, ip := range cleanIPs {
|
||||
if ip == newIP {
|
||||
ipExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add new IP if not seen before
|
||||
if !ipExists {
|
||||
cleanIPs = append(cleanIPs, newIP)
|
||||
}
|
||||
|
||||
// Check if exceeded limit
|
||||
if len(cleanIPs) > limit {
|
||||
return false, nil // Limit exceeded
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RecordIPAccess records or updates IP information
|
||||
func (s *IPLimitService) RecordIPAccess(clientEmail, clientIP string) error {
|
||||
db := database.GetDB()
|
||||
now := time.Now().Unix()
|
||||
|
||||
var record model.InboundClientIPs
|
||||
err := db.Where("client_email = ?", clientEmail).First(&record).Error
|
||||
|
||||
// Parse existing IPs
|
||||
var ipList []string
|
||||
if err == nil {
|
||||
for _, ip := range strings.Split(record.IPs, ",") {
|
||||
if trimmed := strings.TrimSpace(ip); trimmed != "" {
|
||||
ipList = append(ipList, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update IP
|
||||
ipExists := false
|
||||
for _, ip := range ipList {
|
||||
if ip == clientIP {
|
||||
ipExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ipExists {
|
||||
ipList = append(ipList, clientIP)
|
||||
}
|
||||
|
||||
// Join IPs back to string
|
||||
updatedIPs := strings.Join(ipList, ",")
|
||||
|
||||
if err == nil && record.Id > 0 {
|
||||
// Update existing record
|
||||
record.IPs = updatedIPs
|
||||
record.UpdatedAt = now
|
||||
return db.Save(&record).Error
|
||||
}
|
||||
|
||||
// Create new record
|
||||
return db.Create(&model.InboundClientIPs{
|
||||
ClientEmail: clientEmail,
|
||||
IPs: updatedIPs,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetClientIPs returns all IPs for a client
|
||||
func (s *IPLimitService) GetClientIPs(clientEmail string) ([]string, error) {
|
||||
db := database.GetDB()
|
||||
var record model.InboundClientIPs
|
||||
|
||||
err := db.Where("client_email = ?", clientEmail).First(&record).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ips []string
|
||||
for _, ip := range strings.Split(record.IPs, ",") {
|
||||
if trimmed := strings.TrimSpace(ip); trimmed != "" {
|
||||
ips = append(ips, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// RemoveIP removes a specific IP from client's IP list
|
||||
func (s *IPLimitService) RemoveIP(clientEmail, ipToRemove string) error {
|
||||
db := database.GetDB()
|
||||
var record model.InboundClientIPs
|
||||
|
||||
err := db.Where("client_email = ?", clientEmail).First(&record).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse and filter IPs
|
||||
var filtered []string
|
||||
for _, ip := range strings.Split(record.IPs, ",") {
|
||||
if trimmed := strings.TrimSpace(ip); trimmed != "" && trimmed != ipToRemove {
|
||||
filtered = append(filtered, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return db.Delete(&record).Error
|
||||
}
|
||||
|
||||
record.IPs = strings.Join(filtered, ",")
|
||||
record.UpdatedAt = time.Now().Unix()
|
||||
return db.Save(&record).Error
|
||||
}
|
||||
|
||||
// ClearAllIPs removes all IPs for a client
|
||||
func (s *IPLimitService) ClearAllIPs(clientEmail string) error {
|
||||
db := database.GetDB()
|
||||
return db.Where("client_email = ?", clientEmail).Delete(&model.InboundClientIPs{}).Error
|
||||
}
|
||||
Loading…
Reference in a new issue