+
= {
{text: 'Casdoor', link: '/guide/config-casdoor'},
{text: 'Logrotate', link: '/guide/config-logrotate'},
{text: 'Cluster', link: '/guide/config-cluster'},
- {text: 'Auth', link: '/guide/config-auth'}
+ {text: 'Auth', link: '/guide/config-auth'},
+ {text: 'Crypto', link: '/guide/config-crypto'}
]
},
{
diff --git a/docs/.vitepress/config/zh_CN.ts b/docs/.vitepress/config/zh_CN.ts
index 8e5eef974..5e073933e 100644
--- a/docs/.vitepress/config/zh_CN.ts
+++ b/docs/.vitepress/config/zh_CN.ts
@@ -45,7 +45,8 @@ export const zhCNConfig: LocaleSpecificConfig = {
{text: 'Casdoor', link: '/zh_CN/guide/config-casdoor'},
{text: 'Logrotate', link: '/zh_CN/guide/config-logrotate'},
{text: '集群', link: '/zh_CN/guide/config-cluster'},
- {text: '认证', link: '/zh_CN/guide/config-auth'}
+ {text: '认证', link: '/zh_CN/guide/config-auth'},
+ {text: '加密', link: '/zh_CN/guide/config-crypto'}
]
},
{
diff --git a/docs/.vitepress/config/zh_TW.ts b/docs/.vitepress/config/zh_TW.ts
index 24c2c7521..ccf7cca57 100644
--- a/docs/.vitepress/config/zh_TW.ts
+++ b/docs/.vitepress/config/zh_TW.ts
@@ -44,7 +44,8 @@ export const zhTWConfig: LocaleSpecificConfig = {
{text: 'Casdoor', link: '/zh_TW/guide/config-casdoor'},
{text: 'Logrotate', link: '/zh_TW/guide/config-logrotate'},
{text: '集群', link: '/zh_TW/guide/config-cluster'},
- {text: '認證', link: '/zh_TM/guide/config-auth'}
+ {text: '認證', link: '/zh_TW/guide/config-auth'},
+ {text: '加密', link: '/zh_TW/guide/config-crypto'}
]
},
{
diff --git a/docs/guide/config-crypto.md b/docs/guide/config-crypto.md
new file mode 100644
index 000000000..449c267ef
--- /dev/null
+++ b/docs/guide/config-crypto.md
@@ -0,0 +1,7 @@
+# Crypto
+
+## Secret
+- Type: `string`
+
+If this value is empty, Nginx UI will generate a random secret key automatically.
+This secret is used to encrypt the sensitive data stored in the database.
diff --git a/docs/index.md b/docs/index.md
index ffcdf3711..f8ef0f6fa 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -51,6 +51,9 @@ features:
- icon: 📱
title: Responsive Web Design
details: Enjoy a seamless experience on any device with responsive web design.
+ - icon: 🔐
+ title: 2FA Authentication
+ details: Secure sensitive actions with two-factor authentication.
---
diff --git a/docs/zh_CN/guide/config-crypto.md b/docs/zh_CN/guide/config-crypto.md
new file mode 100644
index 000000000..146f61ed3
--- /dev/null
+++ b/docs/zh_CN/guide/config-crypto.md
@@ -0,0 +1,6 @@
+# Crypto
+
+## Secret
+- Type: `string`
+
+如果这个值为空,Nginx UI 将会自动生成一个随机的密钥。这个密钥用于加密存储在数据库中的敏感数据。
diff --git a/docs/zh_CN/index.md b/docs/zh_CN/index.md
index cdeaa2953..f122abee4 100644
--- a/docs/zh_CN/index.md
+++ b/docs/zh_CN/index.md
@@ -51,6 +51,8 @@ features:
- icon: 📱
title: 自适应网页设计
details: 通过自适应网页设计在任何设备上享受无缝体验。
-
+ - icon: 🔐
+ title: 双因素认证
+ details: 使用双因素认证保护敏感操作。
---
diff --git a/docs/zh_TW/guide/config-crypto.md b/docs/zh_TW/guide/config-crypto.md
new file mode 100644
index 000000000..6b582dc95
--- /dev/null
+++ b/docs/zh_TW/guide/config-crypto.md
@@ -0,0 +1,6 @@
+# Crypto
+
+## Secret
+- Type: `string`
+
+如果這個值為空,Nginx UI 將會自動生成一個隨機的密鑰。這個密鑰用於加密存儲在數據庫中的敏感數據。
diff --git a/docs/zh_TW/index.md b/docs/zh_TW/index.md
index 2f5693877..f0b609c3b 100644
--- a/docs/zh_TW/index.md
+++ b/docs/zh_TW/index.md
@@ -51,6 +51,8 @@ features:
- icon: 📱
title: 自適應網頁設計
details: 透過自適應網頁設計在任何裝置上享受無縫體驗。
-
+ - icon: 🔐
+ title: 雙因素認證
+ details: 使用雙因素認證保護敏感操作。
---
diff --git a/go.mod b/go.mod
index d923f55a8..d72ce386c 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
github.com/caarlos0/env/v11 v11.1.0
github.com/casdoor/casdoor-go-sdk v0.47.0
github.com/creack/pty v1.1.21
+ github.com/dgraph-io/ristretto v0.1.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.17.0
@@ -44,7 +45,7 @@ require (
require (
aead.dev/minisign v0.3.0 // indirect
- cloud.google.com/go/auth v0.7.1 // indirect
+ cloud.google.com/go/auth v0.7.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
@@ -73,7 +74,7 @@ require (
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
- github.com/aliyun/alibaba-cloud-sdk-go v1.62.793 // indirect
+ github.com/aliyun/alibaba-cloud-sdk-go v1.62.795 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
@@ -97,6 +98,8 @@ require (
github.com/bytedance/sonic v1.11.9 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+ github.com/cespare/xxhash v1.1.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/civo/civogo v0.3.73 // indirect
github.com/cloudflare/cloudflare-go v0.100.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
@@ -125,20 +128,21 @@ require (
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
- github.com/gofrs/flock v0.12.0 // indirect
+ github.com/gofrs/flock v0.12.1 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+ github.com/golang/glog v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
- github.com/google/s2a-go v0.1.7 // indirect
+ github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
- github.com/googleapis/gax-go/v2 v2.12.5 // indirect
+ github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gophercloud/gophercloud v1.13.0 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/gorilla/css v1.0.1 // indirect
@@ -228,8 +232,8 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/tdewolff/minify/v2 v2.20.37 // indirect
github.com/tdewolff/parse/v2 v2.7.15 // indirect
- github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968 // indirect
- github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968 // indirect
+ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969 // indirect
+ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/transip/gotransip/v6 v6.25.0 // indirect
@@ -242,7 +246,7 @@ require (
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/vultr/govultr/v2 v2.17.2 // indirect
- github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c // indirect
+ github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169 // indirect
github.com/yosssi/ace v0.0.5 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
@@ -264,10 +268,10 @@ require (
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.23.0 // indirect
- google.golang.org/api v0.188.0 // indirect
- google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
+ google.golang.org/api v0.189.0 // indirect
+ google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
@@ -278,6 +282,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/datatypes v1.2.1 // indirect
gorm.io/driver/mysql v1.5.7 // indirect
+ gorm.io/driver/postgres v1.5.6 // indirect
gorm.io/hints v1.1.2 // indirect
k8s.io/api v0.30.3 // indirect
k8s.io/apimachinery v0.30.3 // indirect
diff --git a/go.sum b/go.sum
index 95560c5c7..25f77909e 100644
--- a/go.sum
+++ b/go.sum
@@ -101,8 +101,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
-cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s=
-cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
+cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
+cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
@@ -672,6 +672,7 @@ github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
+github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
@@ -694,8 +695,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/aliyun/alibaba-cloud-sdk-go v1.62.793 h1:7FmdfF5fZMxM8Y0YtwrnMLkwud+egvoB5X5xczqISNQ=
-github.com/aliyun/alibaba-cloud-sdk-go v1.62.793/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.794 h1:M6YtlJdCobRVlJaILK4Eia5aMtDSpeQtxFRl4hSi+DU=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.794/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.795 h1:DjIaInK6Ru+fPnOX0Ef4ux5tkp/dCPI3pAZEijEvlvo=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.795/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
@@ -772,9 +775,12 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -821,8 +827,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.16.3 h1:GT9G86SbQtT1r8ZB+4Cybi9VGdu1P5ieNvNdEoCSbrA=
github.com/deepmap/oapi-codegen v1.16.3/go.mod h1:JD6ErqeX0nYnhdciLc61Konj3NBASREMlkHOgHn8WAM=
+github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA=
+github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
+github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
+github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
@@ -949,6 +961,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY=
github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc=
+github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
+github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -972,6 +986,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
+github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
+github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -1065,6 +1081,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -1092,6 +1110,8 @@ github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
+github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
+github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
@@ -1561,6 +1581,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
@@ -1614,14 +1635,14 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.967 h1:ui73H/2pKk2aDCxaBCLAeMB3JlNgdCkn0nx1x0pqvf0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.967/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968 h1:SdgunZB3WU2vNn3H9dJQ1Z2cQK61vN79zCfnHk3Cu3Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.967 h1:4w33xHFgyrlFZYoGkPQ3uhld8tqoezpObfmCBrdlFBY=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.967/go.mod h1:T0RlPIT2imBeCxLkWfzoiEVP1r5WwzC6becSq7wvSgU=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969 h1:rJlV77WbjuJ5uGBi+THOk09Cfp8Kskz9HgExq0enTmY=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968 h1:h7voJALWRkUX6w7obk9CWHppnJwZuQlreQJVDldVRxY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968/go.mod h1:3cwvPwyqYaYkzAsR4vbrE6mb3Ju9uY7Pj+wHYSVd3aw=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969 h1:W2DHKBCSLjpHoQjqgAkyUu7lV8deIW+FBZS95iNRf1A=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969/go.mod h1:jIxuhjYsAyTTErdwvaX1ay+FHH021fmjdlsbnkaOgfs=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
@@ -1664,6 +1685,8 @@ github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmv
github.com/yandex-cloud/go-genproto v0.0.0-20240701142715-6a03f33f8ec8/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c h1:GzMfpQ/oAP93MOQb5/B+3daDzdcLRRqetZ8radtnJJ4=
github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
+github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa h1:MFb4Q81BMqa0vL64v/i3mel9C+XQkVnwgWqWbmqv10U=
+github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169 h1:5LGYQ/0h1uUo3HH8MsG6R40gvSVPj/7r4D1sKVMa370=
github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169/go.mod h1:kRqpmRyPs8rzXuYEJe57AH546a3VcSjEIzdFa1V66hY=
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
@@ -2051,6 +2074,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -2258,6 +2282,8 @@ google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
+google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
+google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2400,10 +2426,16 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc=
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
+google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=
+google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2515,8 +2547,8 @@ gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
-gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
-gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
+gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU=
+gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
new file mode 100644
index 000000000..97d0466f5
--- /dev/null
+++ b/internal/cache/cache.go
@@ -0,0 +1,31 @@
+package cache
+
+import (
+ "github.com/0xJacky/Nginx-UI/internal/logger"
+ "github.com/dgraph-io/ristretto"
+ "time"
+)
+
+var cache *ristretto.Cache
+
+func Init() {
+ var err error
+ cache, err = ristretto.NewCache(&ristretto.Config{
+ NumCounters: 1e7, // number of keys to track frequency of (10M).
+ MaxCost: 1 << 30, // maximum cost of cache (1GB).
+ BufferItems: 64, // number of keys per Get buffer.
+ })
+
+ if err != nil {
+ logger.Fatal("initializing local cache err", err)
+ }
+}
+
+func Set(key interface{}, value interface{}, ttl time.Duration) {
+ cache.SetWithTTL(key, value, 0, ttl)
+ cache.Wait()
+}
+
+func Get(key interface{}) (value interface{}, ok bool) {
+ return cache.Get(key)
+}
diff --git a/internal/cron/cron.go b/internal/cron/cron.go
index a6b1e1201..decda2e26 100644
--- a/internal/cron/cron.go
+++ b/internal/cron/cron.go
@@ -4,6 +4,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/cert"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/logrotate"
+ "github.com/0xJacky/Nginx-UI/query"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/go-co-op/gocron"
"time"
@@ -25,6 +26,7 @@ func InitCronJobs() {
}
startLogrotate()
+ cleanExpiredAuthToken()
s.StartAsync()
}
@@ -43,10 +45,20 @@ func startLogrotate() {
return
}
var err error
-
logrotateJob, err = s.Every(settings.LogrotateSettings.Interval).Minute().SingletonMode().Do(logrotate.Exec)
-
if err != nil {
logger.Fatalf("LogRotate Job: %v, Err: %v\n", logrotateJob, err)
}
}
+
+func cleanExpiredAuthToken() {
+ job, err := s.Every(5).Minute().SingletonMode().Do(func() {
+ logger.Info("clean expired auth tokens")
+ q := query.AuthToken
+ _, _ = q.Where(q.ExpiredAt.Lt(time.Now().Unix())).Delete()
+ })
+
+ if err != nil {
+ logger.Fatalf("CleanExpiredAuthToken Job: %v, Err: %v\n", job, err)
+ }
+}
diff --git a/internal/crypto/aes.go b/internal/crypto/aes.go
new file mode 100644
index 000000000..1e76d2df8
--- /dev/null
+++ b/internal/crypto/aes.go
@@ -0,0 +1,59 @@
+package crypto
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "github.com/0xJacky/Nginx-UI/settings"
+ "github.com/pkg/errors"
+ "io"
+)
+
+// AesEncrypt encrypts text and given key with AES.
+func AesEncrypt(text []byte) ([]byte, error) {
+ if len(text) == 0 {
+ return nil, errors.New("AesEncrypt text is empty")
+ }
+ block, err := aes.NewCipher(settings.CryptoSettings.GetSecretMd5())
+ if err != nil {
+ return nil, fmt.Errorf("AesEncrypt invalid key: %v", err)
+ }
+
+ b := base64.StdEncoding.EncodeToString(text)
+ ciphertext := make([]byte, aes.BlockSize+len(b))
+ iv := ciphertext[:aes.BlockSize]
+ if _, err = io.ReadFull(rand.Reader, iv); err != nil {
+ return nil, fmt.Errorf("AesEncrypt unable to read IV: %w", err)
+ }
+
+ cfb := cipher.NewCFBEncrypter(block, iv)
+ cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
+
+ return ciphertext, nil
+}
+
+// AesDecrypt decrypts text and given key with AES.
+func AesDecrypt(text []byte) ([]byte, error) {
+ block, err := aes.NewCipher(settings.CryptoSettings.GetSecretMd5())
+ if err != nil {
+ return nil, err
+ }
+
+ if len(text) < aes.BlockSize {
+ return nil, errors.New("AesDecrypt ciphertext too short")
+ }
+
+ iv := text[:aes.BlockSize]
+ text = text[aes.BlockSize:]
+ cfb := cipher.NewCFBDecrypter(block, iv)
+ cfb.XORKeyStream(text, text)
+
+ data, err := base64.StdEncoding.DecodeString(string(text))
+ if err != nil {
+ return nil, fmt.Errorf("AesDecrypt invalid decrypted base64 string: %w", err)
+ }
+
+ return data, nil
+}
diff --git a/internal/crypto/aes_test.go b/internal/crypto/aes_test.go
new file mode 100644
index 000000000..805214c08
--- /dev/null
+++ b/internal/crypto/aes_test.go
@@ -0,0 +1,76 @@
+package crypto
+
+import (
+ "github.com/0xJacky/Nginx-UI/settings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func EncryptDecryptRoundTrip(text string) bool {
+ encrypted, err := AesEncrypt([]byte(text))
+ if err != nil {
+ return false
+ }
+
+ decrypted, err := AesDecrypt(encrypted)
+ if err != nil {
+ return false
+ }
+
+ return text == string(decrypted)
+}
+
+func EncryptsNonEmptyStringWithoutError(text string) bool {
+ _, err := AesEncrypt([]byte(text))
+ return err == nil
+}
+
+func DecryptsToOriginalTextAfterEncryption(text string) bool {
+ encrypted, _ := AesEncrypt([]byte(text))
+ decrypted, err := AesDecrypt(encrypted)
+ if err != nil {
+ return false
+ }
+
+ return text == string(decrypted)
+}
+
+func FailsToDecryptWithModifiedCiphertext(text string) bool {
+ encrypted, _ := AesEncrypt([]byte(text))
+ // Modify the ciphertext
+ encrypted[0] ^= 0xff
+ _, err := AesDecrypt(encrypted)
+ return err != nil
+}
+
+func FailsToDecryptShortCiphertext() bool {
+ _, err := AesDecrypt([]byte("short"))
+ return err != nil
+}
+
+func TestAesEncryptionDecryption(t *testing.T) {
+ settings.CryptoSettings.Secret = "test"
+ assert.True(t, EncryptDecryptRoundTrip("Hello, world!"), "should encrypt and decrypt to the original text")
+ assert.True(t, EncryptsNonEmptyStringWithoutError("Test String"), "should encrypt a non-empty string without error")
+ assert.True(t, DecryptsToOriginalTextAfterEncryption("Another Test String"), "should decrypt to the original text after encryption")
+ assert.True(t, FailsToDecryptWithModifiedCiphertext("Sensitive Data"), "should fail to decrypt with modified ciphertext")
+ assert.True(t, FailsToDecryptShortCiphertext(), "should fail to decrypt short ciphertext")
+}
+
+func TestAesEncrypt_WithEmptyString_ReturnsError(t *testing.T) {
+ settings.CryptoSettings.Secret = "test"
+ _, err := AesEncrypt([]byte(""))
+ require.Error(t, err, "encrypting an empty string should return an error")
+}
+
+func TestAesDecrypt_WithInvalidBase64_ReturnsError(t *testing.T) {
+ settings.CryptoSettings.Secret = "test"
+ // Assuming the function is modified to handle this case explicitly
+ encrypted, _ := AesEncrypt([]byte("valid text"))
+ // Invalidate the base64 encoding
+ encrypted[len(encrypted)-1] = '!'
+ _, err := AesDecrypt(encrypted)
+ require.Error(t, err, "decrypting an invalid base64 string should return an error")
+}
diff --git a/internal/kernal/boot.go b/internal/kernal/boot.go
index 83f81ff07..88753ce90 100644
--- a/internal/kernal/boot.go
+++ b/internal/kernal/boot.go
@@ -1,20 +1,21 @@
package kernal
import (
+ "crypto/rand"
+ "encoding/hex"
"github.com/0xJacky/Nginx-UI/internal/analytic"
+ "github.com/0xJacky/Nginx-UI/internal/cache"
"github.com/0xJacky/Nginx-UI/internal/cert"
"github.com/0xJacky/Nginx-UI/internal/cluster"
+ "github.com/0xJacky/Nginx-UI/internal/cron"
"github.com/0xJacky/Nginx-UI/internal/logger"
- "github.com/0xJacky/Nginx-UI/internal/logrotate"
"github.com/0xJacky/Nginx-UI/internal/validation"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/0xJacky/Nginx-UI/settings"
- "github.com/go-co-op/gocron"
"github.com/google/uuid"
"mime"
"runtime"
- "time"
)
func Boot() {
@@ -24,7 +25,9 @@ func Boot() {
InitJsExtensionType,
InitDatabase,
InitNodeSecret,
+ InitCryptoSecret,
validation.Init,
+ cache.Init,
}
syncs := []func(){
@@ -44,7 +47,7 @@ func InitAfterDatabase() {
syncs := []func(){
registerPredefinedUser,
cert.InitRegister,
- InitCronJobs,
+ cron.InitCronJobs,
cluster.RegisterPredefinedNodes,
analytic.RetrieveNodesStatus,
}
@@ -83,31 +86,34 @@ func InitNodeSecret() {
err := settings.Save()
if err != nil {
- logger.Error("Error save settings")
+ logger.Error("Error save settings", err)
}
logger.Warn("Generated NodeSecret: ", settings.ServerSettings.NodeSecret)
}
}
-func InitJsExtensionType() {
- // Hack: fix wrong Content Type of .js file on some OS platforms
- // See https://github.com/golang/go/issues/32350
- _ = mime.AddExtensionType(".js", "text/javascript; charset=utf-8")
-}
-
-func InitCronJobs() {
- s := gocron.NewScheduler(time.UTC)
- job, err := s.Every(6).Hours().SingletonMode().Do(cert.AutoCert)
+func InitCryptoSecret() {
+ if "" == settings.CryptoSettings.Secret {
+ logger.Warn("Secret is empty, generating...")
- if err != nil {
- logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err)
- }
+ key := make([]byte, 32)
+ if _, err := rand.Read(key); err != nil {
+ logger.Error("Generate Secret failed: ", err)
+ return
+ }
- job, err = s.Every(settings.LogrotateSettings.Interval).Minute().SingletonMode().Do(logrotate.Exec)
+ settings.CryptoSettings.Secret = hex.EncodeToString(key)
- if err != nil {
- logger.Fatalf("LogRotate Job: %v, Err: %v\n", job, err)
+ err := settings.Save()
+ if err != nil {
+ logger.Error("Error save settings", err)
+ }
+ logger.Warn("Secret Generated")
}
+}
- s.StartAsync()
+func InitJsExtensionType() {
+ // Hack: fix wrong Content Type of .js file on some OS platforms
+ // See https://github.com/golang/go/issues/32350
+ _ = mime.AddExtensionType(".js", "text/javascript; charset=utf-8")
}
diff --git a/internal/user/otp.go b/internal/user/otp.go
new file mode 100644
index 000000000..615dc8e5d
--- /dev/null
+++ b/internal/user/otp.go
@@ -0,0 +1,63 @@
+package user
+
+import (
+ "bytes"
+ "crypto/sha1"
+ "encoding/hex"
+ "fmt"
+ "github.com/0xJacky/Nginx-UI/internal/cache"
+ "github.com/0xJacky/Nginx-UI/internal/crypto"
+ "github.com/0xJacky/Nginx-UI/model"
+ "github.com/google/uuid"
+ "github.com/pkg/errors"
+ "github.com/pquerna/otp/totp"
+ "time"
+)
+
+var (
+ ErrOTPCode = errors.New("invalid otp code")
+ ErrRecoveryCode = errors.New("invalid recovery code")
+)
+
+func VerifyOTP(user *model.Auth, otp, recoveryCode string) (err error) {
+ if otp != "" {
+ decrypted, err := crypto.AesDecrypt(user.OTPSecret)
+ if err != nil {
+ return err
+ }
+
+ if ok := totp.Validate(otp, string(decrypted)); !ok {
+ return ErrOTPCode
+ }
+ } else {
+ recoverCode, err := hex.DecodeString(recoveryCode)
+ if err != nil {
+ return err
+ }
+ k := sha1.Sum(user.OTPSecret)
+ if !bytes.Equal(k[:], recoverCode) {
+ return ErrRecoveryCode
+ }
+ }
+ return
+}
+
+func secureSessionIDCacheKey(sessionId string) string {
+ return fmt.Sprintf("otp_secure_session:_%s", sessionId)
+}
+
+func SetSecureSessionID(userId int) (sessionId string) {
+ sessionId = uuid.NewString()
+ cache.Set(secureSessionIDCacheKey(sessionId), userId, 5*time.Minute)
+
+ return
+}
+
+func VerifySecureSessionID(sessionId string, userId int) bool {
+ if v, ok := cache.Get(secureSessionIDCacheKey(sessionId)); ok {
+ if v.(int) == userId {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/user/user.go b/internal/user/user.go
index 325fe7da8..75b37c5ed 100644
--- a/internal/user/user.go
+++ b/internal/user/user.go
@@ -1,53 +1,84 @@
package user
import (
+ "github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/model"
+ "github.com/0xJacky/Nginx-UI/query"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/dgrijalva/jwt-go"
+ "github.com/pkg/errors"
+ "strings"
"time"
)
+const ExpiredTime = 24 * time.Hour
+
type JWTClaims struct {
- Name string `json:"name"`
+ Name string `json:"name"`
+ UserID int `json:"user_id"`
jwt.StandardClaims
}
-func GetUser(name string) (user model.Auth, err error) {
+func BuildCacheTokenKey(token string) string {
+ var sb strings.Builder
+ sb.WriteString("token:")
+ sb.WriteString(token)
+ return sb.String()
+}
+
+func GetUser(name string) (user *model.Auth, err error) {
db := model.UseDB()
- err = db.Where("name", name).First(&user).Error
+ user = &model.Auth{}
+ err = db.Where("name", name).First(user).Error
if err != nil {
return
}
return
}
-func DeleteToken(token string) error {
- db := model.UseDB()
- return db.Where("token", token).Delete(&model.AuthToken{}).Error
+func DeleteToken(token string) {
+ q := query.AuthToken
+ _, _ = q.Where(q.Token.Eq(token)).Delete()
}
-func CheckToken(token string) int64 {
- db := model.UseDB()
- return db.Where("token", token).Find(&model.AuthToken{}).RowsAffected
+func GetTokenUser(token string) (*model.Auth, bool) {
+ q := query.AuthToken
+ authToken, err := q.Where(q.Token.Eq(token)).First()
+ if err != nil {
+ return nil, false
+ }
+
+ if authToken.ExpiredAt < time.Now().Unix() {
+ DeleteToken(token)
+ return nil, false
+ }
+
+ u := query.Auth
+ user, err := u.FirstByID(authToken.UserID)
+ return user, err == nil
}
-func GenerateJWT(name string) (string, error) {
+func GenerateJWT(user *model.Auth) (string, error) {
claims := JWTClaims{
- Name: name,
+ Name: user.Name,
+ UserID: user.ID,
StandardClaims: jwt.StandardClaims{
- ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
+ ExpiresAt: time.Now().Add(ExpiredTime).Unix(),
},
}
+
unsignedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := unsignedToken.SignedString([]byte(settings.ServerSettings.JwtSecret))
if err != nil {
return "", err
}
- db := model.UseDB()
- err = db.Create(&model.AuthToken{
- Token: signedToken,
- }).Error
+ q := query.AuthToken
+ err = q.Create(&model.AuthToken{
+ UserID: user.ID,
+ Token: signedToken,
+ ExpiredAt: time.Now().Add(ExpiredTime).Unix(),
+ })
if err != nil {
return "", err
@@ -55,3 +86,50 @@ func GenerateJWT(name string) (string, error) {
return signedToken, err
}
+
+func ValidateJWT(token string) (claims *JWTClaims, err error) {
+ if token == "" {
+ err = errors.New("token is empty")
+ return
+ }
+ unsignedToken, err := jwt.ParseWithClaims(
+ token,
+ &JWTClaims{},
+ func(token *jwt.Token) (interface{}, error) {
+ return []byte(settings.ServerSettings.JwtSecret), nil
+ },
+ )
+ if err != nil {
+ err = errors.New("parse with claims error")
+ return
+ }
+ claims, ok := unsignedToken.Claims.(*JWTClaims)
+ if !ok {
+ err = errors.New("convert to jwt claims error")
+ return
+ }
+ if claims.ExpiresAt < time.Now().UTC().Unix() {
+ err = errors.New("jwt is expired")
+ }
+ return
+}
+
+func CurrentUser(token string) (u *model.Auth, err error) {
+ // validate token
+ var claims *JWTClaims
+ claims, err = ValidateJWT(token)
+ if err != nil {
+ return
+ }
+
+ // get user by id
+ user := query.Auth
+ u, err = user.FirstByID(claims.UserID)
+ if err != nil {
+ return
+ }
+
+ logger.Info("[Current User]", u.Name)
+
+ return
+}
diff --git a/model/auth.go b/model/auth.go
index 45720b522..c463f754a 100644
--- a/model/auth.go
+++ b/model/auth.go
@@ -1,13 +1,20 @@
package model
type Auth struct {
- Model
+ Model
- Name string `json:"name"`
- Password string `json:"-"`
- Status bool `json:"status" gorm:"default:1"`
+ Name string `json:"name"`
+ Password string `json:"-"`
+ Status bool `json:"status" gorm:"default:1"`
+ OTPSecret []byte `json:"-" gorm:"type:blob"`
}
type AuthToken struct {
- Token string `json:"token"`
+ UserID int `json:"user_id"`
+ Token string `json:"token"`
+ ExpiredAt int64 `json:"expired_at" gorm:"default:0"`
+}
+
+func (u *Auth) EnabledOTP() bool {
+ return len(u.OTPSecret) != 0
}
diff --git a/query/auth_tokens.gen.go b/query/auth_tokens.gen.go
index b06d7ced8..56b24d8da 100644
--- a/query/auth_tokens.gen.go
+++ b/query/auth_tokens.gen.go
@@ -28,7 +28,9 @@ func newAuthToken(db *gorm.DB, opts ...gen.DOOption) authToken {
tableName := _authToken.authTokenDo.TableName()
_authToken.ALL = field.NewAsterisk(tableName)
+ _authToken.UserID = field.NewInt(tableName, "user_id")
_authToken.Token = field.NewString(tableName, "token")
+ _authToken.ExpiredAt = field.NewInt64(tableName, "expired_at")
_authToken.fillFieldMap()
@@ -38,8 +40,10 @@ func newAuthToken(db *gorm.DB, opts ...gen.DOOption) authToken {
type authToken struct {
authTokenDo
- ALL field.Asterisk
- Token field.String
+ ALL field.Asterisk
+ UserID field.Int
+ Token field.String
+ ExpiredAt field.Int64
fieldMap map[string]field.Expr
}
@@ -56,7 +60,9 @@ func (a authToken) As(alias string) *authToken {
func (a *authToken) updateTableName(table string) *authToken {
a.ALL = field.NewAsterisk(table)
+ a.UserID = field.NewInt(table, "user_id")
a.Token = field.NewString(table, "token")
+ a.ExpiredAt = field.NewInt64(table, "expired_at")
a.fillFieldMap()
@@ -73,8 +79,10 @@ func (a *authToken) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (a *authToken) fillFieldMap() {
- a.fieldMap = make(map[string]field.Expr, 1)
+ a.fieldMap = make(map[string]field.Expr, 3)
+ a.fieldMap["user_id"] = a.UserID
a.fieldMap["token"] = a.Token
+ a.fieldMap["expired_at"] = a.ExpiredAt
}
func (a authToken) clone(db *gorm.DB) authToken {
diff --git a/query/auths.gen.go b/query/auths.gen.go
index d27499dbb..1295fb716 100644
--- a/query/auths.gen.go
+++ b/query/auths.gen.go
@@ -35,6 +35,7 @@ func newAuth(db *gorm.DB, opts ...gen.DOOption) auth {
_auth.Name = field.NewString(tableName, "name")
_auth.Password = field.NewString(tableName, "password")
_auth.Status = field.NewBool(tableName, "status")
+ _auth.OTPSecret = field.NewBytes(tableName, "otp_secret")
_auth.fillFieldMap()
@@ -52,6 +53,7 @@ type auth struct {
Name field.String
Password field.String
Status field.Bool
+ OTPSecret field.Bytes
fieldMap map[string]field.Expr
}
@@ -75,6 +77,7 @@ func (a *auth) updateTableName(table string) *auth {
a.Name = field.NewString(table, "name")
a.Password = field.NewString(table, "password")
a.Status = field.NewBool(table, "status")
+ a.OTPSecret = field.NewBytes(table, "otp_secret")
a.fillFieldMap()
@@ -91,7 +94,7 @@ func (a *auth) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (a *auth) fillFieldMap() {
- a.fieldMap = make(map[string]field.Expr, 7)
+ a.fieldMap = make(map[string]field.Expr, 8)
a.fieldMap["id"] = a.ID
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["updated_at"] = a.UpdatedAt
@@ -99,6 +102,7 @@ func (a *auth) fillFieldMap() {
a.fieldMap["name"] = a.Name
a.fieldMap["password"] = a.Password
a.fieldMap["status"] = a.Status
+ a.fieldMap["otp_secret"] = a.OTPSecret
}
func (a auth) clone(db *gorm.DB) auth {
diff --git a/router/middleware.go b/router/middleware.go
index bcf7944cc..70ad8a89b 100644
--- a/router/middleware.go
+++ b/router/middleware.go
@@ -5,6 +5,7 @@ import (
"github.com/0xJacky/Nginx-UI/app"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/user"
+ "github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
@@ -58,11 +59,14 @@ func authRequired() gin.HandlerFunc {
}
}
- if user.CheckToken(token) < 1 {
+ u, ok := user.GetTokenUser(token)
+ if !ok {
abortWithAuthFailure()
return
}
+ c.Set("user", u)
+
if nodeID := c.GetHeader("X-Node-ID"); nodeID != "" {
c.Set("ProxyNodeID", nodeID)
}
@@ -71,6 +75,41 @@ func authRequired() gin.HandlerFunc {
}
}
+func required2FA() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ u, ok := c.Get("user")
+ if !ok {
+ c.Next()
+ return
+ }
+ cUser := u.(*model.Auth)
+ if !cUser.EnabledOTP() {
+ c.Next()
+ return
+ }
+ ssid := c.GetHeader("X-Secure-Session-ID")
+ if ssid == "" {
+ ssid = c.Query("X-Secure-Session-ID")
+ }
+ if ssid == "" {
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
+ "message": "Secure Session ID is empty",
+ })
+ return
+ }
+
+ if user.VerifySecureSessionID(ssid, cUser.ID) {
+ c.Next()
+ return
+ }
+
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
+ "message": "Secure Session ID is invalid",
+ })
+ return
+ }
+}
+
type serverFileSystemType struct {
http.FileSystem
}
diff --git a/router/routers.go b/router/routers.go
index eb15a257a..ad5630ceb 100644
--- a/router/routers.go
+++ b/router/routers.go
@@ -46,6 +46,7 @@ func InitRouter() *gin.Engine {
// Authorization required not websocket request
g := root.Group("/", authRequired(), proxy())
{
+ user.InitUserRouter(g)
analytic.InitRouter(g)
user.InitManageUserRouter(g)
nginx.InitRouter(g)
@@ -68,7 +69,10 @@ func InitRouter() *gin.Engine {
{
analytic.InitWebSocketRouter(w)
certificate.InitCertificateWebSocketRouter(w)
- terminal.InitRouter(w)
+ o := w.Group("", required2FA())
+ {
+ terminal.InitRouter(o)
+ }
nginx.InitNginxLogRouter(w)
upstream.InitRouter(w)
system.InitWebSocketRouter(w)
diff --git a/settings/cluster_test.go b/settings/cluster_test.go
index 1d01ed0c7..235b47b6d 100644
--- a/settings/cluster_test.go
+++ b/settings/cluster_test.go
@@ -11,5 +11,6 @@ func TestCluster(t *testing.T) {
assert.Equal(t, []string{
"http://10.0.0.1:9000?name=node1&node_secret=my-node-secret&enabled=true",
"http://10.0.0.2:9000?name=node2&node_secret=my-node-secret&enabled=true",
+ "http://10.0.0.3?name=node3&node_secret=my-node-secret&enabled=true",
}, ClusterSettings.Node)
}
diff --git a/settings/crypto.go b/settings/crypto.go
new file mode 100644
index 000000000..57d5e0c66
--- /dev/null
+++ b/settings/crypto.go
@@ -0,0 +1,14 @@
+package settings
+
+import "crypto/md5"
+
+type Crypto struct {
+ Secret string
+}
+
+var CryptoSettings = Crypto{}
+
+func (c *Crypto) GetSecretMd5() []byte {
+ k := md5.Sum([]byte(c.Secret))
+ return k[:]
+}
diff --git a/settings/crypto_test.go b/settings/crypto_test.go
new file mode 100644
index 000000000..db10bae5a
--- /dev/null
+++ b/settings/crypto_test.go
@@ -0,0 +1,48 @@
+package settings
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetSecretMd5_WithNonEmptySecret_ReturnsExpectedMd5Hash(t *testing.T) {
+ // Setup
+ CryptoSettings.Secret = "testSecret"
+ expectedMd5 := md5.Sum([]byte("testSecret"))
+ expectedMd5String := hex.EncodeToString(expectedMd5[:])
+
+ // Execute
+ resultMd5 := CryptoSettings.GetSecretMd5()
+ resultMd5String := hex.EncodeToString(resultMd5[:])
+
+ // Verify
+ assert.Equal(t, expectedMd5String, resultMd5String, "MD5 hash should match for non-empty secret")
+}
+
+func TestGetSecretMd5_WithEmptySecret_ReturnsMd5OfEmptyString(t *testing.T) {
+ // Setup
+ CryptoSettings.Secret = ""
+ expectedMd5 := md5.Sum([]byte(""))
+ expectedMd5String := hex.EncodeToString(expectedMd5[:])
+
+ // Execute
+ resultMd5 := CryptoSettings.GetSecretMd5()
+ resultMd5String := hex.EncodeToString(resultMd5[:])
+
+ // Verify
+ assert.Equal(t, expectedMd5String, resultMd5String, "MD5 hash of an empty string should be returned for empty secret")
+}
+
+func TestGetSecretMd5_WithDifferentSecrets_ReturnsDifferentMd5Hashes(t *testing.T) {
+ // Setup
+ CryptoSettings.Secret = "secret1"
+ firstMd5 := CryptoSettings.GetSecretMd5()
+ CryptoSettings.Secret = "secret2"
+ secondMd5 := CryptoSettings.GetSecretMd5()
+
+ // Verify
+ assert.NotEqual(t, firstMd5, secondMd5, "Different secrets should produce different MD5 hashes")
+}
diff --git a/settings/settings.go b/settings/settings.go
index 12c3c6bfc..b29bbbc43 100644
--- a/settings/settings.go
+++ b/settings/settings.go
@@ -28,6 +28,7 @@ var sections = map[string]interface{}{
"logrotate": &LogrotateSettings,
"cluster": &ClusterSettings,
"auth": &AuthSettings,
+ "crypto": &CryptoSettings,
}
func init() {
@@ -64,6 +65,7 @@ func Setup() {
parseEnv(&CasdoorSettings, "CASDOOR_")
parseEnv(&LogrotateSettings, "LOGROTATE_")
parseEnv(&AuthSettings, "AUTH_")
+ parseEnv(&CryptoSettings, "CRYPTO_")
// if in official docker, set the restart cmd of nginx to "nginx -s stop",
// then the supervisor of s6-overlay will start the nginx again.