Skip to content

Commit fb2ea7e

Browse files
authored
Merge pull request #439 from 0xJacky/enhance/login
Enhance/login
2 parents 204ffae + 743daaa commit fb2ea7e

Some content is hidden

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

63 files changed

+4362
-2695
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: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,92 @@ package user
22

33
import (
44
"github.com/0xJacky/Nginx-UI/api"
5-
"github.com/0xJacky/Nginx-UI/model"
5+
"github.com/0xJacky/Nginx-UI/internal/logger"
6+
"github.com/0xJacky/Nginx-UI/internal/user"
7+
"github.com/0xJacky/Nginx-UI/query"
8+
"github.com/0xJacky/Nginx-UI/settings"
9+
"github.com/gin-gonic/gin"
10+
"github.com/pkg/errors"
611
"net/http"
12+
"sync"
713
"time"
8-
9-
"github.com/gin-gonic/gin"
10-
"golang.org/x/crypto/bcrypt"
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"`
1621
}
1722

23+
const (
24+
ErrPasswordIncorrect = 4031
25+
ErrMaxAttempts = 4291
26+
ErrUserBanned = 4033
27+
)
28+
1829
type LoginResponse struct {
1930
Message string `json:"message"`
20-
Token string `json:"token"`
31+
Error string `json:"error,omitempty"`
32+
Code int `json:"code"`
33+
Token string `json:"token,omitempty"`
2134
}
2235

2336
func Login(c *gin.Context) {
24-
var user LoginUser
25-
ok := api.BindAndValid(c, &user)
26-
if !ok {
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+
})
2753
return
2854
}
2955

30-
u, _ := model.GetUser(user.Name)
56+
var json LoginUser
57+
ok := api.BindAndValid(c, &json)
58+
if !ok {
59+
return
60+
}
3161

32-
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(user.Password)); err != nil {
33-
time.Sleep(5 * time.Second)
34-
c.JSON(http.StatusForbidden, gin.H{
35-
"message": "The username or password is incorrect",
36-
})
62+
u, err := user.Login(json.Name, json.Password)
63+
if err != nil {
64+
// time.Sleep(5 * time.Second)
65+
switch {
66+
case errors.Is(err, user.ErrPasswordIncorrect):
67+
c.JSON(http.StatusForbidden, LoginResponse{
68+
Message: "Password incorrect",
69+
Code: ErrPasswordIncorrect,
70+
})
71+
case errors.Is(err, user.ErrUserBanned):
72+
c.JSON(http.StatusForbidden, LoginResponse{
73+
Message: "The user is banned",
74+
Code: ErrUserBanned,
75+
})
76+
default:
77+
api.ErrHandler(c, err)
78+
}
79+
user.BanIP(clientIP)
3780
return
3881
}
3982

40-
token, err := model.GenerateJWT(u.Name)
83+
// login success, clear banned record
84+
_, _ = b.Where(b.IP.Eq(clientIP)).Delete()
85+
86+
logger.Info("[User Login]", u.Name)
87+
token, err := user.GenerateJWT(u.Name)
4188
if err != nil {
42-
c.JSON(http.StatusInternalServerError, gin.H{
43-
"message": err.Error(),
89+
c.JSON(http.StatusInternalServerError, LoginResponse{
90+
Message: err.Error(),
4491
})
4592
return
4693
}
@@ -54,7 +101,7 @@ func Login(c *gin.Context) {
54101
func Logout(c *gin.Context) {
55102
token := c.GetHeader("Authorization")
56103
if token != "" {
57-
err := model.DeleteToken(token)
104+
err := user.DeleteToken(token)
58105
if err != nil {
59106
c.JSON(http.StatusInternalServerError, gin.H{
60107
"message": err.Error(),

api/user/casdoor.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package user
33
import (
44
"fmt"
55
"github.com/0xJacky/Nginx-UI/api"
6-
"github.com/0xJacky/Nginx-UI/model"
6+
"github.com/0xJacky/Nginx-UI/internal/user"
77
"github.com/0xJacky/Nginx-UI/settings"
88
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
99
"github.com/gin-gonic/gin"
@@ -53,7 +53,7 @@ func CasdoorCallback(c *gin.Context) {
5353
return
5454
}
5555

56-
u, err := model.GetUser(claims.Name)
56+
u, err := user.GetUser(claims.Name)
5757
if err != nil {
5858
if errors.Is(err, gorm.ErrRecordNotFound) {
5959
c.JSON(http.StatusForbidden, gin.H{
@@ -65,7 +65,7 @@ func CasdoorCallback(c *gin.Context) {
6565
return
6666
}
6767

68-
userToken, err := model.GenerateJWT(u.Name)
68+
userToken, err := user.GenerateJWT(u.Name)
6969
if err != nil {
7070
api.ErrHandler(c, err)
7171
return

app/.eslintrc.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ module.exports = {
88
'plugin:vue/vue3-recommended',
99
'plugin:import/recommended',
1010
'plugin:import/typescript',
11-
'plugin:promise/recommended',
1211
'plugin:sonarjs/recommended',
1312
'plugin:@typescript-eslint/recommended',
1413

app/package.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "2.0.0-beta.25",
44
"type": "module",
55
"scripts": {
6-
"dev": "vite",
6+
"dev": "vite --host",
77
"typecheck": "vue-tsc --noEmit",
88
"lint": "eslint . -c .eslintrc.cjs --fix --ext .ts,.vue,.tsx,.d.ts",
99
"build": "vite build",
@@ -13,17 +13,17 @@
1313
"dependencies": {
1414
"@ant-design/icons-vue": "^7.0.1",
1515
"@formkit/auto-animate": "^0.8.2",
16-
"@vue/reactivity": "^3.4.29",
17-
"@vue/shared": "^3.4.29",
16+
"@vue/reactivity": "^3.4.33",
17+
"@vue/shared": "^3.4.33",
1818
"@vueuse/core": "^10.11.0",
1919
"@xterm/addon-attach": "^0.11.0",
2020
"@xterm/addon-fit": "^0.10.0",
2121
"@xterm/xterm": "^5.5.0",
2222
"ant-design-vue": "^4.2.3",
23-
"apexcharts": "^3.49.1",
23+
"apexcharts": "^3.50.0",
2424
"axios": "^1.7.2",
25-
"dayjs": "^1.11.11",
26-
"highlight.js": "^11.9.0",
25+
"dayjs": "^1.11.12",
26+
"highlight.js": "^11.10.0",
2727
"lodash": "^4.17.21",
2828
"marked": "^10.0.0",
2929
"nprogress": "^0.2.0",
@@ -32,42 +32,42 @@
3232
"reconnecting-websocket": "^4.4.0",
3333
"sortablejs": "^1.15.2",
3434
"vite-plugin-build-id": "^0.2.9",
35-
"vue": "^3.4.29",
35+
"vue": "^3.4.33",
3636
"vue-github-button": "github:0xJacky/vue-github-button",
37-
"vue-router": "^4.3.3",
37+
"vue-router": "^4.4.0",
3838
"vue3-ace-editor": "2.2.4",
3939
"vue3-apexcharts": "1.4.4",
4040
"vue3-gettext": "3.0.0-beta.4",
4141
"vuedraggable": "^4.1.0"
4242
},
4343
"devDependencies": {
4444
"@antfu/eslint-config-vue": "^0.43.1",
45-
"@types/lodash": "^4.17.5",
45+
"@types/lodash": "^4.17.7",
4646
"@types/nprogress": "^0.2.3",
4747
"@types/sortablejs": "^1.15.8",
4848
"@typescript-eslint/eslint-plugin": "^6.21.0",
4949
"@typescript-eslint/parser": "^6.21.0",
5050
"@vitejs/plugin-vue": "^5.0.5",
5151
"@vitejs/plugin-vue-jsx": "^3.1.0",
52-
"@vue/compiler-sfc": "^3.4.29",
52+
"@vue/compiler-sfc": "^3.4.33",
5353
"@vue/tsconfig": "^0.5.1",
54-
"ace-builds": "^1.35.0",
54+
"ace-builds": "^1.35.3",
5555
"autoprefixer": "^10.4.19",
5656
"eslint": "^8.57.0",
5757
"eslint-import-resolver-alias": "^1.1.2",
5858
"eslint-import-resolver-typescript": "^3.6.1",
5959
"eslint-plugin-import": "^2.29.1",
6060
"eslint-plugin-regex": "^1.10.0",
6161
"eslint-plugin-sonarjs": "^0.23.0",
62-
"eslint-plugin-vue": "^9.26.0",
62+
"eslint-plugin-vue": "^9.27.0",
6363
"less": "^4.2.0",
64-
"postcss": "^8.4.38",
65-
"tailwindcss": "^3.4.4",
64+
"postcss": "^8.4.39",
65+
"tailwindcss": "^3.4.6",
6666
"typescript": "5.3.3",
67-
"unplugin-auto-import": "^0.17.6",
67+
"unplugin-auto-import": "^0.17.8",
6868
"unplugin-vue-components": "^0.26.0",
6969
"unplugin-vue-define-options": "^1.4.5",
70-
"vite": "^5.3.1",
70+
"vite": "^5.3.4",
7171
"vite-svg-loader": "^5.1.0",
7272
"vue-tsc": "^1.8.27"
7373
},

0 commit comments

Comments
 (0)