added parameters to list bans and unban ip from terminal

This commit is contained in:
ghostersk
2026-03-08 18:07:19 +00:00
parent d6e987f66c
commit f122d29282
5 changed files with 134 additions and 9 deletions
+78 -1
View File
@@ -47,6 +47,16 @@ func main() {
} }
runDisableMFA(args[1]) runDisableMFA(args[1])
return return
case "--blocklist":
runBlockList()
return
case "--unblock":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: gowebmail --unblock <ip>")
os.Exit(1)
}
runUnblock(args[1])
return
case "--help", "-h": case "--help", "-h":
printHelp() printHelp()
return return
@@ -335,6 +345,69 @@ func runDisableMFA(username string) {
fmt.Printf("MFA disabled for admin '%s'. They can now log in with password only.\n", username) fmt.Printf("MFA disabled for admin '%s'. They can now log in with password only.\n", username)
} }
func runBlockList() {
database, close := openDB()
defer close()
blocks, err := database.ListIPBlocksWithUsername()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if len(blocks) == 0 {
fmt.Println("No blocked IPs.")
return
}
fmt.Printf("%-18s %-20s %-5s %-22s %-22s %s\n",
"IP", "USERNAME USED", "TRIES", "BLOCKED AT", "EXPIRES", "REMAINING")
fmt.Printf("%-18s %-20s %-5s %-22s %-22s %s\n",
"--", "-------------", "-----", "----------", "-------", "---------")
for _, b := range blocks {
blockedAt := b.BlockedAt.UTC().Format("2006-01-02 15:04:05")
var expires, remaining string
if b.IsPermanent || b.ExpiresAt == nil {
expires = "permanent"
remaining = "∞ (manual unblock)"
} else {
expires = b.ExpiresAt.UTC().Format("2006-01-02 15:04:05")
left := time.Until(*b.ExpiresAt)
if left <= 0 {
remaining = "expired (purge pending)"
} else {
h := int(left.Hours())
m := int(left.Minutes()) % 60
s := int(left.Seconds()) % 60
if h > 0 {
remaining = fmt.Sprintf("%dh %dm", h, m)
} else if m > 0 {
remaining = fmt.Sprintf("%dm %ds", m, s)
} else {
remaining = fmt.Sprintf("%ds", s)
}
}
}
username := b.LastUsername
if username == "" {
username = "(unknown)"
}
fmt.Printf("%-18s %-20s %-5d %-22s %-22s %s\n",
b.IP, username, b.Attempts, blockedAt, expires, remaining)
}
fmt.Printf("\nTotal: %d blocked IP(s)\n", len(blocks))
}
func runUnblock(ip string) {
database, close := openDB()
defer close()
if err := database.UnblockIP(ip); err != nil {
fmt.Fprintf(os.Stderr, "Error unblocking %s: %v\n", ip, err)
os.Exit(1)
}
fmt.Printf("IP %s has been unblocked.\n", ip)
}
func printHelp() { func printHelp() {
fmt.Print(`GoWebMail — Admin CLI fmt.Print(`GoWebMail — Admin CLI
@@ -343,13 +416,17 @@ Usage:
gowebmail --list-admin List all admin accounts (username, email, MFA status) gowebmail --list-admin List all admin accounts (username, email, MFA status)
gowebmail --pw <username> <pass> Reset password for an admin account gowebmail --pw <username> <pass> Reset password for an admin account
gowebmail --mfa-off <username> Disable MFA for an admin account gowebmail --mfa-off <username> Disable MFA for an admin account
gowebmail --blocklist List all currently blocked IP addresses
gowebmail --unblock <ip> Remove block for a specific IP address
Examples: Examples:
./gowebmail --list-admin ./gowebmail --list-admin
./gowebmail --pw admin "NewSecurePass123" ./gowebmail --pw admin "NewSecurePass123"
./gowebmail --mfa-off admin ./gowebmail --mfa-off admin
./gowebmail --blocklist
./gowebmail --unblock 1.2.3.4
Note: These commands only work on admin accounts. Note: --list-admin, --pw, and --mfa-off only work on admin accounts.
Regular user management is done through the web UI. Regular user management is done through the web UI.
Requires the same environment variables as the server (DB_PATH, ENCRYPTION_KEY, etc). Requires the same environment variables as the server (DB_PATH, ENCRYPTION_KEY, etc).
`) `)
+52 -4
View File
@@ -1838,11 +1838,11 @@ func (d *DB) ListIPBlocks() ([]IPBlock, error) {
var result []IPBlock var result []IPBlock
for rows.Next() { for rows.Next() {
var b IPBlock var b IPBlock
var expiresStr sql.NullString var expiresAt sql.NullTime
rows.Scan(&b.ID, &b.IP, &b.Reason, &b.Country, &b.CountryCode, rows.Scan(&b.ID, &b.IP, &b.Reason, &b.Country, &b.CountryCode,
&b.Attempts, &b.BlockedAt, &expiresStr, &b.IsPermanent) &b.Attempts, &b.BlockedAt, &expiresAt, &b.IsPermanent)
if expiresStr.Valid { if expiresAt.Valid {
t, _ := time.Parse("2006-01-02 15:04:05", expiresStr.String) t := expiresAt.Time
b.ExpiresAt = &t b.ExpiresAt = &t
} }
result = append(result, b) result = append(result, b)
@@ -1988,3 +1988,51 @@ func splitIPs(s string) []string {
} }
return result return result
} }
// IPBlockWithUsername extends IPBlock with the last username attempted from that IP.
type IPBlockWithUsername struct {
IPBlock
LastUsername string
}
// ListIPBlocksWithUsername returns active blocks enriched with the most recent
// username that was attempted from each IP (from login_attempts history).
func (d *DB) ListIPBlocksWithUsername() ([]IPBlockWithUsername, error) {
rows, err := d.sql.Query(`
SELECT
b.id, b.ip, b.reason, b.country, b.country_code,
b.attempts, b.blocked_at, b.expires_at, b.is_permanent,
COALESCE(
(SELECT username FROM login_attempts
WHERE ip=b.ip AND username != ''
ORDER BY created_at DESC LIMIT 1),
''
) AS last_username
FROM ip_blocks b
WHERE b.is_permanent=1 OR b.expires_at IS NULL OR b.expires_at > datetime('now')
ORDER BY b.blocked_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var result []IPBlockWithUsername
for rows.Next() {
var b IPBlockWithUsername
var expiresAt sql.NullTime
err := rows.Scan(
&b.ID, &b.IP, &b.Reason, &b.Country, &b.CountryCode,
&b.Attempts, &b.BlockedAt, &expiresAt, &b.IsPermanent,
&b.LastUsername,
)
if err != nil {
return nil, err
}
if expiresAt.Valid {
t := expiresAt.Time
b.ExpiresAt = &t
}
result = append(result, b)
}
return result, rows.Err()
}
+1 -1
View File
@@ -39,5 +39,5 @@
{{end}} {{end}}
{{define "scripts"}} {{define "scripts"}}
<script src="/static/js/admin.js?v=21"></script> <script src="/static/js/admin.js?v=23"></script>
{{end}} {{end}}
+1 -1
View File
@@ -352,5 +352,5 @@
{{end}} {{end}}
{{define "scripts"}} {{define "scripts"}}
<script src="/static/js/app.js?v=21"></script> <script src="/static/js/app.js?v=23"></script>
{{end}} {{end}}
+2 -2
View File
@@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}GoWebMail{{end}}</title> <title>{{block "title" .}}GoWebMail{{end}}</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,400&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/gowebmail.css?v=21"> <link rel="stylesheet" href="/static/css/gowebmail.css?v=23">
{{block "head_extra" .}}{{end}} {{block "head_extra" .}}{{end}}
</head> </head>
<body class="{{block "body_class" .}}{{end}}"> <body class="{{block "body_class" .}}{{end}}">
{{block "body" .}}{{end}} {{block "body" .}}{{end}}
<script src="/static/js/gowebmail.js?v=21"></script> <script src="/static/js/gowebmail.js?v=23"></script>
{{block "scripts" .}}{{end}} {{block "scripts" .}}{{end}}
</body> </body>
</html> </html>