Skip to content

Commit cff843b

Browse files
committed
feat: add login failed ban ip list
1 parent ccb04c0 commit cff843b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2258
-953
lines changed

api/settings/auth.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package settings
2+
3+
import (
4+
"github.com/0xJacky/Nginx-UI/api"
5+
"github.com/0xJacky/Nginx-UI/query"
6+
"github.com/0xJacky/Nginx-UI/settings"
7+
"github.com/gin-gonic/gin"
8+
"net/http"
9+
"time"
10+
)
11+
12+
func GetBanLoginIP(c *gin.Context) {
13+
b := query.BanIP
14+
15+
// clear expired banned IPs
16+
_, _ = b.Where(b.ExpiredAt.Lte(time.Now().Unix())).Delete()
17+
18+
banIps, err := b.Where(
19+
b.ExpiredAt.Gte(time.Now().Unix()),
20+
b.Attempts.Gte(settings.AuthSettings.MaxAttempts)).Find()
21+
if err != nil {
22+
api.ErrHandler(c, err)
23+
return
24+
}
25+
c.JSON(http.StatusOK, banIps)
26+
}
27+
28+
func RemoveBannedIP(c *gin.Context) {
29+
var json struct {
30+
IP string `json:"ip"`
31+
}
32+
if !api.BindAndValid(c, &json) {
33+
return
34+
}
35+
36+
b := query.BanIP
37+
_, err := b.Where(b.IP.Eq(json.IP)).Delete()
38+
39+
if err != nil {
40+
api.ErrHandler(c, err)
41+
return
42+
}
43+
44+
c.JSON(http.StatusNoContent, nil)
45+
}

api/settings/router.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package settings
2+
3+
import (
4+
"github.com/gin-gonic/gin"
5+
)
6+
7+
func InitRouter(r *gin.RouterGroup) {
8+
r.GET("settings/server/name", GetServerName)
9+
r.GET("settings", GetSettings)
10+
r.POST("settings", SaveSettings)
11+
12+
r.GET("settings/auth/banned_ips", GetBanLoginIP)
13+
r.DELETE("settings/auth/banned_ip", RemoveBannedIP)
14+
}

api/system/settings.go renamed to api/settings/settings.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package system
1+
package settings
22

33
import (
44
"github.com/0xJacky/Nginx-UI/api"
@@ -20,6 +20,7 @@ func GetSettings(c *gin.Context) {
2020
"nginx": settings.NginxSettings,
2121
"openai": settings.OpenAISettings,
2222
"logrotate": settings.LogrotateSettings,
23+
"auth": settings.AuthSettings,
2324
})
2425
}
2526

@@ -29,6 +30,7 @@ func SaveSettings(c *gin.Context) {
2930
Nginx settings.Nginx `json:"nginx"`
3031
Openai settings.OpenAI `json:"openai"`
3132
Logrotate settings.Logrotate `json:"logrotate"`
33+
Auth settings.Auth `json:"auth"`
3234
}
3335

3436
if !api.BindAndValid(c, &json) {
@@ -44,6 +46,7 @@ func SaveSettings(c *gin.Context) {
4446
settings.ProtectedFill(&settings.NginxSettings, &json.Nginx)
4547
settings.ProtectedFill(&settings.OpenAISettings, &json.Openai)
4648
settings.ProtectedFill(&settings.LogrotateSettings, &json.Logrotate)
49+
settings.ProtectedFill(&settings.AuthSettings, &json.Auth)
4750

4851
err := settings.Save()
4952
if err != nil {

api/system/router.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package system
22

33
import (
4-
"github.com/gin-gonic/gin"
4+
"github.com/gin-gonic/gin"
55
)
66

77
func InitPublicRouter(r *gin.RouterGroup) {
8-
r.GET("install", InstallLockCheck)
9-
r.POST("install", InstallNginxUI)
10-
r.GET("translation/:code", GetTranslation)
8+
r.GET("install", InstallLockCheck)
9+
r.POST("install", InstallNginxUI)
10+
r.GET("translation/:code", GetTranslation)
1111
}
1212

1313
func InitPrivateRouter(r *gin.RouterGroup) {
14-
r.GET("settings/server/name", GetServerName)
15-
r.GET("settings", GetSettings)
16-
r.POST("settings", SaveSettings)
17-
18-
r.GET("upgrade/release", GetRelease)
19-
r.GET("upgrade/current", GetCurrentVersion)
20-
r.GET("upgrade/perform", PerformCoreUpgrade)
14+
r.GET("upgrade/release", GetRelease)
15+
r.GET("upgrade/current", GetCurrentVersion)
16+
r.GET("upgrade/perform", PerformCoreUpgrade)
2117
}

api/user/auth.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import (
44
"github.com/0xJacky/Nginx-UI/api"
55
"github.com/0xJacky/Nginx-UI/internal/logger"
66
"github.com/0xJacky/Nginx-UI/internal/user"
7+
"github.com/0xJacky/Nginx-UI/query"
8+
"github.com/0xJacky/Nginx-UI/settings"
79
"github.com/gin-gonic/gin"
810
"github.com/pkg/errors"
911
"net/http"
12+
"sync"
1013
"time"
1114
)
1215

16+
var mutex = &sync.Mutex{}
17+
1318
type LoginUser struct {
1419
Name string `json:"name" binding:"required,max=255"`
1520
Password string `json:"password" binding:"required,max=255"`
@@ -29,6 +34,25 @@ type LoginResponse struct {
2934
}
3035

3136
func Login(c *gin.Context) {
37+
// make sure that only one request is processed at a time
38+
mutex.Lock()
39+
defer mutex.Unlock()
40+
// check if the ip is banned
41+
clientIP := c.ClientIP()
42+
b := query.BanIP
43+
banIP, _ := b.Where(b.IP.Eq(clientIP),
44+
b.ExpiredAt.Gte(time.Now().Unix()),
45+
b.Attempts.Gte(settings.AuthSettings.MaxAttempts),
46+
).Count()
47+
48+
if banIP > 0 {
49+
c.JSON(http.StatusTooManyRequests, LoginResponse{
50+
Message: "Max attempts",
51+
Code: ErrMaxAttempts,
52+
})
53+
return
54+
}
55+
3256
var json LoginUser
3357
ok := api.BindAndValid(c, &json)
3458
if !ok {
@@ -37,7 +61,7 @@ func Login(c *gin.Context) {
3761

3862
u, err := user.Login(json.Name, json.Password)
3963
if err != nil {
40-
time.Sleep(5 * time.Second)
64+
// time.Sleep(5 * time.Second)
4165
switch {
4266
case errors.Is(err, user.ErrPasswordIncorrect):
4367
c.JSON(http.StatusForbidden, LoginResponse{
@@ -52,9 +76,13 @@ func Login(c *gin.Context) {
5276
default:
5377
api.ErrHandler(c, err)
5478
}
79+
user.BanIP(clientIP)
5580
return
5681
}
5782

83+
// login success, clear banned record
84+
_, _ = b.Where(b.IP.Eq(clientIP)).Delete()
85+
5886
logger.Info("[User Login]", u.Name)
5987
token, err := user.GenerateJWT(u.Name)
6088
if err != nil {

app/src/api/curd.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface Pagination {
1313
total_pages: number
1414
}
1515

16-
export interface IGetListResponse<T> {
16+
export interface GetListResponse<T> {
1717
data: T[]
1818
pagination: Pagination
1919
}
@@ -35,7 +35,7 @@ class Curd<T> {
3535
}
3636

3737
// eslint-disable-next-line @typescript-eslint/no-explicit-any
38-
_get_list(params: any = null): Promise<IGetListResponse<T>> {
38+
_get_list(params: any = null): Promise<GetListResponse<T>> {
3939
return http.get(this.plural, { params })
4040
}
4141

app/src/api/settings.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import http from '@/lib/http'
22

3+
export interface BannedIP {
4+
ip: string
5+
attempts: number
6+
expired_at: string
7+
}
8+
39
const settings = {
4-
get() {
10+
get<T>(): Promise<T> {
511
return http.get('/settings')
612
},
7-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8-
save(data: any) {
13+
save<T>(data: T) {
914
return http.post('/settings', data)
1015
},
11-
12-
get_server_name() {
16+
get_server_name(): Promise<{ name: string }> {
1317
return http.get('/settings/server/name')
1418
},
19+
get_banned_ips(): Promise<BannedIP[]> {
20+
return http.get('/settings/auth/banned_ips')
21+
},
22+
remove_banned_ip(ip: string) {
23+
return http.delete('/settings/auth/banned_ip', { data: { ip } })
24+
},
1525
}
1626

1727
export default settings

app/src/language/LINGUAS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
es fr_FR ko_KR ru_RU vi_VN zh_CN zh_TW
1+
en zh_CN zh_TW fr_FR es ru_RU vi_VN ko_KR

0 commit comments

Comments
 (0)