You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/08/21 10:45:29 UTC
[apisix-dashboard] branch master updated: feat(authentication):
create authentication module (#330)
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 98d488b feat(authentication): create authentication module (#330)
98d488b is described below
commit 98d488bc45c8e293911588d51caf73c89e678e2f
Author: bzp2010 <bz...@gmail.com>
AuthorDate: Fri Aug 21 18:45:18 2020 +0800
feat(authentication): create authentication module (#330)
* feat(authentication): create module typing definition
* feat(authentication): create Login page
* feat(authentication): update typing definition
* feat(authentication): add centent to Login page
* feat(authentication): update typing definition
* feat(authentication): update Login page to add Password and Test method
* feat(authentication): update typing definition to add check and submit function
* feat(authentication): move Test login method to Example
* feat(authentication): add check and submit function
* feat(authentication): add submit function in Login page
* feat(authentication): add test to Password login method
* feat(authentication): change example LoginMethod text
* feat(authentication): add i18n content
* feat(authentication): redirect to index when login success
* feat(i18n): update i18n file import
remove import i18n file of user module manually and try auto import by umi.js
* feat(authentication): create authentication configure items
* fix(authentication): fix logging filter
write back request body for read by PostForm function
* feat(authentication): create authentication controller
* feat(authentication): update dependencies
* fix(authentication): fix logging filter
* feat(authentication): change to session for authentication
* feat(authentication): create authentication filter
use authentication filter to check every request
* feat(authentication): create unit test case
* fix(authentication): change HTTP code when authentication fail request
* feat(authentication): add jwt dependency
* feat(authentication): create session configures
* feat(authentication): change cookie-based session to jwt
* feat(authentication): change cors Access-Control-Allow-Headers header
* feat(authentication): change login page path and error handler
* feat(authentication): create request interceptor to add Authorization header
* feat(authentication): connect to backend login API and i18n
* feat(authentication): create logout page
* feat(authentication): add redirect query to back previous page
* feat(authentication): update LoginMethod definition for logout
* feat(authentication): add logout button
* feat(authentication): improve login page
* fix: clean codes
* fix(authentication): fix unit test crash
* feat(authentication): remove API url setting
* feat(authentication): improve session check
* feat(authentication): redirect to login page when not exist token
* fix: clean codes and add ASF header
---
api/conf/conf.go | 35 ++++++
api/conf/conf.json | 16 +++
api/errno/error.go | 4 +
api/filter/authentication.go | 66 ++++++++++
api/filter/cors.go | 2 +-
api/filter/logging.go | 5 +
api/go.mod | 5 +-
api/go.sum | 40 ++++++
api/route/authentication.go | 68 +++++++++++
api/route/{base_test.go => authentication_test.go} | 40 ++++--
api/route/base.go | 16 ++-
api/route/base_test.go | 7 +-
api/route/consumer_test.go | 1 +
config/routes.ts | 10 ++
src/app.tsx | 20 ++-
src/components/RightContent/AvatarDropdown.tsx | 21 +++-
src/helpers.tsx | 19 ++-
src/locales/en-US/component.ts | 9 --
src/locales/zh-CN/component.ts | 9 --
src/pages/Setting/Setting.tsx | 4 +-
src/pages/User/Login.less | 133 ++++++++++++++++++++
src/pages/User/Login.tsx | 123 +++++++++++++++++++
.../base_test.go => src/pages/User/Logout.tsx | 40 +++---
.../pages/User/components/LoginMethodExample.tsx | 45 ++++---
src/pages/User/components/LoginMethodPassword.tsx | 135 +++++++++++++++++++++
src/pages/User/index.ts | 2 +
src/pages/User/locales/en-US.ts | 14 +++
src/pages/User/locales/zh-CN.ts | 13 ++
src/pages/User/typing.d.ts | 25 ++++
29 files changed, 841 insertions(+), 86 deletions(-)
diff --git a/api/conf/conf.go b/api/conf/conf.go
index f4d7b77..1ed7ecb 100644
--- a/api/conf/conf.go
+++ b/api/conf/conf.go
@@ -45,6 +45,7 @@ func init() {
setEnvironment()
initMysql()
initApisix()
+ initAuthentication()
}
func setEnvironment() {
@@ -74,7 +75,22 @@ type mysqlConfig struct {
MaxLifeTime int
}
+type user struct {
+ Username string
+ Password string
+}
+
+type authenticationConfig struct {
+ Session struct {
+ Secret string
+ ExpireTime uint64
+ }
+}
+
+var UserList = make(map[string]user, 1)
+
var MysqlConfig mysqlConfig
+var AuthenticationConfig authenticationConfig
func initMysql() {
filePath := configurationPath()
@@ -103,3 +119,22 @@ func initApisix() {
ApiKey = apisixConf.Get("api_key").String()
}
}
+
+func initAuthentication() {
+ filePath := configurationPath()
+ if configurationContent, err := ioutil.ReadFile(filePath); err != nil {
+ panic(fmt.Sprintf("fail to read configuration: %s", filePath))
+ } else {
+ configuration := gjson.ParseBytes(configurationContent)
+ userList := configuration.Get("authentication.user").Array()
+
+ // create user list
+ for _, item := range userList{
+ username := item.Map()["username"].String()
+ password := item.Map()["password"].String()
+ UserList[item.Map()["username"].String()] = user{Username: username, Password: password}
+ }
+ AuthenticationConfig.Session.Secret = configuration.Get("authentication.session.secret").String()
+ AuthenticationConfig.Session.ExpireTime = configuration.Get("authentication.session.expireTime").Uint()
+ }
+}
diff --git a/api/conf/conf.json b/api/conf/conf.json
index 31731c3..d160087 100644
--- a/api/conf/conf.json
+++ b/api/conf/conf.json
@@ -15,5 +15,21 @@
"base_url": "http://127.0.0.1:9080/apisix/admin",
"api_key": "edd1c9f034335f136f87ad84b625c8f1"
}
+ },
+ "authentication": {
+ "session": {
+ "secret": "secret",
+ "expireTime": 3600
+ },
+ "user": [
+ {
+ "username": "admin",
+ "password": "admin"
+ },
+ {
+ "username": "user",
+ "password": "user"
+ }
+ ]
}
}
diff --git a/api/errno/error.go b/api/errno/error.go
index 08b5adb..93180d8 100644
--- a/api/errno/error.go
+++ b/api/errno/error.go
@@ -42,6 +42,7 @@ var (
InvalidParamDetail = Message{"010010", "Invalid request parameter: %s", 400}
AdminApiSaveError = Message{"010011", "Data save failed", 500}
SchemaCheckFailed = Message{"010012", "%s", 400}
+ ForbiddenError = Message{"010013", "Request Unauthorized", 401}
//BB 01 configuration
ConfEnvError = Message{"010101", "Environment variable not found: %s", 500}
@@ -84,6 +85,9 @@ var (
ApisixConsumerUpdateError = Message{"010703", "APISIX Consumer update failed", 500}
ApisixConsumerDeleteError = Message{"010704", "APISIX Consumer delete failed", 500}
DuplicateUserName = Message{"010705", "Duplicate consumer username", 400}
+
+ // 99 authentication
+ AuthenticationUserError = Message{"019901", "username or password error", 401}
)
type ManagerError struct {
diff --git a/api/filter/authentication.go b/api/filter/authentication.go
new file mode 100644
index 0000000..11c1561
--- /dev/null
+++ b/api/filter/authentication.go
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package filter
+
+import (
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "strings"
+)
+
+func Authentication() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if c.Request.URL.Path != "/user/login" && strings.HasPrefix(c.Request.URL.Path,"/apisix") {
+ tokenStr := c.GetHeader("Authorization")
+
+ // verify token
+ token, err := jwt.ParseWithClaims(tokenStr, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
+ return []byte(conf.AuthenticationConfig.Session.Secret), nil
+ })
+
+ if err != nil {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ claims, ok := token.Claims.(*jwt.StandardClaims)
+ if !ok {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ if err := token.Claims.Valid(); err != nil {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ if claims.Subject == "" {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+
+ if _, ok := conf.UserList[claims.Subject]; !ok {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.ForbiddenError).Response())
+ return
+ }
+ }
+ c.Next()
+ }
+}
diff --git a/api/filter/cors.go b/api/filter/cors.go
index f2c7ac1..b33c62b 100644
--- a/api/filter/cors.go
+++ b/api/filter/cors.go
@@ -22,7 +22,7 @@ func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
- c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
+ c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization")
c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
diff --git a/api/filter/logging.go b/api/filter/logging.go
index b1a40f6..6318d52 100644
--- a/api/filter/logging.go
+++ b/api/filter/logging.go
@@ -18,6 +18,7 @@ package filter
import (
"bytes"
+ "io/ioutil"
"time"
"github.com/apisix/manager-api/errno"
@@ -33,11 +34,15 @@ func RequestLogHandler() gin.HandlerFunc {
val = c.Request.URL.Query()
} else {
val, _ = c.GetRawData()
+
+ // set RequestBody back
+ c.Request.Body = ioutil.NopCloser(bytes.NewReader(val.([]byte)))
}
c.Set("requestBody", val)
uuid, _ := c.Get("X-Request-Id")
param, _ := c.Get("requestBody")
+
switch param.(type) {
case []byte:
param = string(param.([]byte))
diff --git a/api/go.mod b/api/go.mod
index ab07faa..0ebf639 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -4,12 +4,15 @@ go 1.13
require (
github.com/api7/apitest v1.4.9
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/pprof v1.3.0
+ github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.6.3
- github.com/go-sql-driver/mysql v1.5.0
+ github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/jinzhu/gorm v1.9.12
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.6.0
+ github.com/steinfletcher/apitest v1.4.9 // indirect
github.com/stretchr/testify v1.6.1
github.com/tidwall/gjson v1.6.0
gopkg.in/resty.v1 v1.12.0
diff --git a/api/go.sum b/api/go.sum
index 1daa324..b82d22e 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,20 +1,34 @@
github.com/api7/apitest v1.4.9 h1:FYTUQJ1hgeB9UvMFif1jjbfiA+XqHPEBfsjhDskytA8=
github.com/api7/apitest v1.4.9/go.mod h1:YZruZ+jDMFL6rNgMWiuhwCTugNN0mJkLCYCHG3ICYlE=
+github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
+github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
+github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+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/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
+github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
+github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
@@ -22,36 +36,57 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
+github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
+github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/steinfletcher/apitest v1.4.9 h1:8X7G+1m+GngIo5LFfDM0CxLSG9jcJn9LLeDH/Ov144M=
+github.com/steinfletcher/apitest v1.4.9/go.mod h1:0MT98QwexQVvf5pIn3fqiC/+8Nyd7A4RShxuSjnpOcE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -71,6 +106,7 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -79,13 +115,17 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/api/route/authentication.go b/api/route/authentication.go
new file mode 100644
index 0000000..c19fa6a
--- /dev/null
+++ b/api/route/authentication.go
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ jwt "github.com/dgrijalva/jwt-go"
+ "github.com/gin-gonic/gin"
+ "net/http"
+ "time"
+)
+
+type UserSession struct {
+ Token string `json:"token"`
+}
+
+func AppendAuthentication(r *gin.Engine) *gin.Engine {
+ r.POST("/user/login", userLogin)
+ return r
+}
+
+func userLogin(c *gin.Context) {
+ username := c.PostForm("username")
+ password := c.PostForm("password")
+
+ if username == "" {
+ c.AbortWithStatusJSON(http.StatusBadRequest, errno.FromMessage(errno.InvalidParamDetail, "username is needed").Response())
+ return
+ }
+ if password == "" {
+ c.AbortWithStatusJSON(http.StatusBadRequest, errno.FromMessage(errno.InvalidParamDetail, "password is needed").Response())
+ return
+ }
+
+ user := conf.UserList[username]
+ if username != user.Username || password != user.Password {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, errno.FromMessage(errno.AuthenticationUserError).Response())
+ } else {
+ // create JWT for session
+ claims := jwt.StandardClaims{
+ Subject: username,
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(time.Second * time.Duration(conf.AuthenticationConfig.Session.ExpireTime)).Unix(),
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ signedToken, _ := token.SignedString([]byte(conf.AuthenticationConfig.Session.Secret))
+
+ // output token
+ c.AbortWithStatusJSON(http.StatusOK, errno.FromMessage(errno.SystemSuccess).ItemResponse(&UserSession {
+ Token: signedToken,
+ }))
+ }
+}
diff --git a/api/route/base_test.go b/api/route/authentication_test.go
similarity index 52%
copy from api/route/base_test.go
copy to api/route/authentication_test.go
index 3e3a096..0cc0214 100644
--- a/api/route/base_test.go
+++ b/api/route/authentication_test.go
@@ -17,22 +17,36 @@
package route
import (
- "github.com/api7/apitest"
-
- "github.com/apisix/manager-api/conf"
+ "bytes"
+ "net/http"
+ "strings"
+ "testing"
)
-var handler *apitest.APITest
-
-var (
- uriPrefix = "/apisix/admin"
-)
+var token string
-func init() {
- //init mysql connect
- conf.InitializeMysql()
+func TestUserLogin(t *testing.T) {
+ // password error
+ handler.
+ Post("/user/login").
+ Header("Content-Type", "application/x-www-form-urlencoded").
+ Body("username=admin&password=admin1").
+ Expect(t).
+ Status(http.StatusUnauthorized).
+ End()
- r := SetUpRouter()
+ // login success
+ sessionResponse := handler.
+ Post("/user/login").
+ Header("Content-Type", "application/x-www-form-urlencoded").
+ Body("username=admin&password=admin").
+ Expect(t).
+ Status(http.StatusOK).
+ End().Response.Body
- handler = apitest.New().Handler(r)
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(sessionResponse)
+ data := buf.String()
+ tokenArr := strings.Split(data, "\"token\":\"")
+ token = strings.Split(tokenArr[1], "\"}")[0]
}
diff --git a/api/route/base.go b/api/route/base.go
index 51dd90b..1b48d4a 100644
--- a/api/route/base.go
+++ b/api/route/base.go
@@ -17,11 +17,13 @@
package route
import (
- "github.com/gin-contrib/pprof"
- "github.com/gin-gonic/gin"
+ "github.com/gin-contrib/pprof"
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-contrib/sessions/cookie"
+ "github.com/gin-gonic/gin"
- "github.com/apisix/manager-api/conf"
- "github.com/apisix/manager-api/filter"
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/filter"
)
func SetUpRouter() *gin.Engine {
@@ -31,10 +33,12 @@ func SetUpRouter() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
-
- r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
+ store := cookie.NewStore([]byte("secret"))
+ r.Use(sessions.Sessions("session", store))
+ r.Use(filter.CORS(), filter.Authentication(),filter.RequestId(), filter.RequestLogHandler(), filter.RecoverHandler())
AppendHealthCheck(r)
+ AppendAuthentication(r)
AppendRoute(r)
AppendSsl(r)
AppendPlugin(r)
diff --git a/api/route/base_test.go b/api/route/base_test.go
index 3e3a096..007af13 100644
--- a/api/route/base_test.go
+++ b/api/route/base_test.go
@@ -18,7 +18,6 @@ package route
import (
"github.com/api7/apitest"
-
"github.com/apisix/manager-api/conf"
)
@@ -34,5 +33,9 @@ func init() {
r := SetUpRouter()
- handler = apitest.New().Handler(r)
+ handler = apitest.
+ New().
+ Handler(r)
}
+
+
diff --git a/api/route/consumer_test.go b/api/route/consumer_test.go
index 60066bb..b32c38f 100644
--- a/api/route/consumer_test.go
+++ b/api/route/consumer_test.go
@@ -27,6 +27,7 @@ func TestConsumer(t *testing.T) {
// create ok
handler.
Post(uriPrefix + "/consumers").
+ Header("Authorization", token).
JSON(`{
"username": "e2e_test_consumer1",
"plugins": {
diff --git a/config/routes.ts b/config/routes.ts
index 9851a95..6648413 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -76,6 +76,16 @@ const routes = [
component: './Setting',
},
{
+ path: '/user/login',
+ component: './User/Login',
+ layout: false,
+ },
+ {
+ path: '/user/logout',
+ component: './User/Logout',
+ layout: false,
+ },
+ {
component: './404',
},
];
diff --git a/src/app.tsx b/src/app.tsx
index d309fac..b437cc9 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import React from 'react';
-import { RequestConfig } from 'umi';
+import { RequestConfig, history } from 'umi';
import {
BasicLayoutProps,
Settings as LayoutSettings,
@@ -32,6 +32,11 @@ export async function getInitialState(): Promise<{
currentUser?: API.CurrentUser;
settings?: LayoutSettings;
}> {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ history.replace(`/user/login?redirect=${encodeURIComponent(window.location.pathname)}`);
+ }
+
const currentUser = await queryCurrent();
return {
currentUser,
@@ -59,4 +64,17 @@ export const request: RequestConfig = {
prefix: '/apisix/admin',
errorHandler,
credentials: 'same-origin',
+ requestInterceptors: [
+ (url, options) => {
+ const newOptions = options;
+ newOptions.headers = {
+ ...options.headers,
+ Authorization: localStorage.getItem('token') || '',
+ };
+ return {
+ url,
+ options: { ...newOptions, interceptors: true },
+ };
+ },
+ ],
};
diff --git a/src/components/RightContent/AvatarDropdown.tsx b/src/components/RightContent/AvatarDropdown.tsx
index b3fbf9d..88c8c39 100644
--- a/src/components/RightContent/AvatarDropdown.tsx
+++ b/src/components/RightContent/AvatarDropdown.tsx
@@ -22,9 +22,8 @@
* SOFTWARE.
*/
import React, { useCallback } from 'react';
-import { SettingOutlined, UserOutlined, SettingFilled } from '@ant-design/icons';
+import { SettingOutlined, UserOutlined, SettingFilled, LogoutOutlined } from '@ant-design/icons';
import { Avatar, Menu, Spin } from 'antd';
-import { ClickParam } from 'antd/es/menu';
import { history, useModel } from 'umi';
import { stringify } from 'querystring';
@@ -50,13 +49,24 @@ const settings = async () => {
const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
const { initialState, setInitialState } = useModel('@@initialState');
- const onMenuClick = useCallback((event: ClickParam) => {
+ const onMenuClick = useCallback((event) => {
const { key } = event;
if (key === 'settings') {
setInitialState({ ...initialState, currentUser: undefined });
settings();
return;
}
+
+ if (key === 'logout') {
+ setInitialState({ ...initialState, currentUser: undefined });
+ history.replace({
+ pathname: '/user/logout',
+ search: stringify({
+ redirect: window.location.pathname,
+ }),
+ });
+ return;
+ }
history.push(`/account/${key}`);
}, []);
@@ -102,6 +112,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
<SettingFilled />
修改设置
</Menu.Item>
+ <Menu.Divider />
+ <Menu.Item key="logout">
+ <LogoutOutlined />
+ 退出
+ </Menu.Item>
</Menu>
);
return (
diff --git a/src/helpers.tsx b/src/helpers.tsx
index f644c22..39e97da 100644
--- a/src/helpers.tsx
+++ b/src/helpers.tsx
@@ -17,6 +17,7 @@
import React from 'react';
import { notification } from 'antd';
import { MenuDataItem } from '@ant-design/pro-layout';
+import { history } from 'umi';
import { codeMessage } from './constants';
import IconFont from './iconfont';
@@ -56,7 +57,7 @@ export const getMenuData = (): MenuDataItem[] => {
];
};
-export const isLoginPage = () => window.location.pathname.indexOf('/login') !== -1;
+export const isLoginPage = () => window.location.pathname.indexOf('/user/login') !== -1;
/**
* 异常处理程序
@@ -65,9 +66,10 @@ export const errorHandler = (error: { response: Response; data: any }): Promise<
const { response } = error;
if (response && response.status) {
if ([401].includes(response.status) && !isLoginPage()) {
- localStorage.clear();
- window.location.href = '/login';
+ history.replace(`/user/logout?redirect=${encodeURIComponent(window.location.pathname)}`);
+ return Promise.reject(response);
}
+ if ([401].includes(response.status) && isLoginPage()) return Promise.reject(response);
const errorText =
error.data.msg || error.data.message || error.data.error_msg || codeMessage[response.status];
@@ -84,3 +86,14 @@ export const errorHandler = (error: { response: Response; data: any }): Promise<
}
return Promise.reject(response);
};
+
+export const getUrlQuery: (key: string) => string | false = (key: string) => {
+ const query = window.location.search.substring(1);
+ const vars = query.split('&');
+
+ for (let i = 0; i < vars.length; i += 1) {
+ const pair = vars[i].split('=');
+ if (pair[0] === key) return pair[1];
+ }
+ return false;
+};
diff --git a/src/locales/en-US/component.ts b/src/locales/en-US/component.ts
index fc5e8da..85b2a4b 100644
--- a/src/locales/en-US/component.ts
+++ b/src/locales/en-US/component.ts
@@ -32,15 +32,6 @@ export default {
'component.global.loading': 'Loading',
'component.status.success': 'Successfully',
'component.status.fail': 'Failed',
- // User component
- 'component.user.loginByPassword': 'Username & Password',
- 'component.user.login': 'Login',
- 'component.user.username': 'Username',
- 'component.user.password': 'Password',
- 'component.user.rememberMe': 'Remember Me',
- 'component.user.inputUsername': 'Please input username!',
- 'component.user.inputPassword': 'Please input password!',
- 'component.user.wrongUsernameOrPassword': 'Wrong account or password!',
// SSL Module
'component.ssl.removeSSLItemModalContent': 'You are going to remove this item!',
'component.ssl.removeSSLItemModalTitle': 'SSL Remove Alert',
diff --git a/src/locales/zh-CN/component.ts b/src/locales/zh-CN/component.ts
index f2ca2d8..0e4f487 100644
--- a/src/locales/zh-CN/component.ts
+++ b/src/locales/zh-CN/component.ts
@@ -32,15 +32,6 @@ export default {
'component.global.loading': '加载中',
'component.status.success': '成功',
'component.status.fail': '失败',
- // User component
- 'component.user.loginByPassword': '账号密码登录',
- 'component.user.login': '登录',
- 'component.user.username': '账号',
- 'component.user.password': '密码',
- 'component.user.rememberMe': '自动登录',
- 'component.user.inputUsername': '请输入账号!',
- 'component.user.inputPassword': '请输入密码!',
- 'component.user.wrongUsernameOrPassword': '账号或密码错误!',
// SSL Module
'component.ssl.removeSSLItemModalContent': '确定要删除该项吗?',
'component.ssl.removeSSLItemModalTitle': '删除 SSL',
diff --git a/src/pages/Setting/Setting.tsx b/src/pages/Setting/Setting.tsx
index 6b95859..65c1dab 100644
--- a/src/pages/Setting/Setting.tsx
+++ b/src/pages/Setting/Setting.tsx
@@ -19,6 +19,7 @@ import { PageContainer } from '@ant-design/pro-layout';
import { Card, Form, Input, Row, Col, notification } from 'antd';
import { useIntl } from 'umi';
+import { getUrlQuery } from '@/helpers';
import ActionBar from '@/components/ActionBar';
import { getGrafanaURL } from '@/pages/Metrics/service';
@@ -60,7 +61,8 @@ const Setting: React.FC = () => {
}),
});
setTimeout(() => {
- window.location.reload();
+ const redirect = getUrlQuery('redirect');
+ window.location.href = redirect ? decodeURIComponent(redirect) : '/';
}, 500);
});
};
diff --git a/src/pages/User/Login.less b/src/pages/User/Login.less
new file mode 100644
index 0000000..8635c96
--- /dev/null
+++ b/src/pages/User/Login.less
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '~antd/es/style/themes/default.less';
+
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: auto;
+ background: @layout-body-background;
+}
+
+.lang {
+ width: 100%;
+ height: 42px;
+ line-height: 42px;
+ text-align: right;
+}
+
+.settings {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px;
+ font-size: 16px;
+ vertical-align: middle;
+ cursor: pointer;
+}
+
+.settings-icon {
+ font-size: 18px;
+}
+
+.content {
+ flex: 1;
+ padding: 32px 0;
+}
+
+@media (min-width: @screen-md-min) {
+ .container {
+ background-repeat: no-repeat;
+ background-position: center 110px;
+ background-size: 100%;
+ }
+
+ .content {
+ padding: 32px 0 24px;
+ }
+}
+
+.top {
+ text-align: center;
+}
+
+.header {
+ height: 44px;
+ line-height: 44px;
+
+ a {
+ text-decoration: none;
+ }
+}
+
+.logo {
+ height: 44px;
+ margin-right: 16px;
+ vertical-align: top;
+}
+
+.title {
+ position: relative;
+ top: 2px;
+ color: @heading-color;
+ font-weight: 600;
+ font-size: 33px;
+ font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
+}
+
+.desc {
+ margin-top: 12px;
+ margin-bottom: 40px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+}
+
+.main {
+ width: 368px;
+ margin: 0 auto;
+ @media screen and (max-width: @screen-sm) {
+ width: 95%;
+ }
+
+ .icon {
+ margin-left: 16px;
+ color: rgba(0, 0, 0, 0.2);
+ font-size: 24px;
+ vertical-align: middle;
+ cursor: pointer;
+ transition: color 0.3s;
+
+ &:hover {
+ color: @primary-color;
+ }
+ }
+
+ .submit {
+ width: 100%;
+ }
+
+ .other {
+ margin-top: 24px;
+ line-height: 22px;
+ text-align: left;
+
+ .register {
+ float: right;
+ }
+ }
+}
diff --git a/src/pages/User/Login.tsx b/src/pages/User/Login.tsx
new file mode 100644
index 0000000..56542d5
--- /dev/null
+++ b/src/pages/User/Login.tsx
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useState } from 'react';
+import { Button, notification, Tabs } from 'antd';
+import { DefaultFooter } from '@ant-design/pro-layout';
+import { SelectLang } from '@@/plugin-locale/SelectLang';
+import { Link, useIntl, history } from 'umi';
+import LoginMethodPassword from '@/pages/User/components/LoginMethodPassword';
+import LoginMethodExample from '@/pages/User/components/LoginMethodExample';
+import { UserModule } from '@/pages/User/typing';
+import logo from '@/assets/logo.svg';
+import { SettingOutlined } from '@ant-design/icons';
+import styles from './Login.less';
+import { getUrlQuery } from '@/helpers';
+
+const Tab = Tabs.TabPane;
+
+/**
+ * Login Methods List
+ */
+const loginMethods: UserModule.LoginMethod[] = [LoginMethodPassword, LoginMethodExample];
+
+/**
+ * User Login Page
+ * @constructor
+ */
+const Page: React.FC = () => {
+ const { formatMessage } = useIntl();
+ const [loginMethod, setLoginMethod] = useState(loginMethods[0]);
+
+ const onSettingsClick = () => {
+ history.replace(`/settings?redirect=${encodeURIComponent(history.location.pathname)}`);
+ };
+
+ const onTabChange = (activeKey: string) => {
+ loginMethods.forEach((item, index) => {
+ if (activeKey === item.id) setLoginMethod(loginMethods[index]);
+ });
+ };
+
+ const onSubmit = () => {
+ loginMethod.checkData().then((validate) => {
+ if (validate) {
+ loginMethod.submit(loginMethod.getData()).then((response) => {
+ if (response.status) {
+ notification.success({
+ message: formatMessage({ id: 'component.status.success' }),
+ description: response.message,
+ duration: 1,
+ onClose: () => {
+ const redirect = getUrlQuery('redirect');
+ history.replace(redirect ? decodeURIComponent(redirect) : '/');
+ },
+ });
+ } else {
+ notification.error({
+ message: formatMessage({ id: 'component.status.fail' }),
+ description: response.message,
+ });
+ }
+ });
+ }
+ });
+ };
+
+ if (localStorage.getItem('token')) {
+ history.replace('/');
+ return null;
+ }
+ return (
+ <div className={styles.container}>
+ <div className={styles.lang}>
+ <div className={styles.settings} onClick={onSettingsClick}>
+ <SettingOutlined />
+ </div>
+ <SelectLang />
+ </div>
+ <div className={styles.content}>
+ <div className={styles.top}>
+ <div className={styles.header}>
+ <Link to="/">
+ <img alt="logo" className={styles.logo} src={logo} />
+ </Link>
+ </div>
+ <div className={styles.desc}>
+ Apache APISIX Dashboard
+ <br />
+ Cloud-Native Microservices API Gateway
+ </div>
+ </div>
+ <div className={styles.main}>
+ <Tabs activeKey={loginMethod.id} onChange={onTabChange}>
+ {loginMethods.map((item) => (
+ <Tab key={item.id} tab={item.name}>
+ {item.render()}
+ </Tab>
+ ))}
+ </Tabs>
+ <Button className={styles.submit} size="large" type="primary" onClick={onSubmit}>
+ {formatMessage({ id: 'component.user.login' })}
+ </Button>
+ </div>
+ </div>
+ <DefaultFooter />
+ </div>
+ );
+};
+
+export default Page;
diff --git a/api/route/base_test.go b/src/pages/User/Logout.tsx
similarity index 52%
copy from api/route/base_test.go
copy to src/pages/User/Logout.tsx
index 3e3a096..9f64c2a 100644
--- a/api/route/base_test.go
+++ b/src/pages/User/Logout.tsx
@@ -14,25 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package route
+import React from 'react';
+import LoginMethodPassword from '@/pages/User/components/LoginMethodPassword';
+import LoginMethodExample from '@/pages/User/components/LoginMethodExample';
+import { UserModule } from '@/pages/User/typing';
+import { getUrlQuery } from '@/helpers';
-import (
- "github.com/api7/apitest"
-
- "github.com/apisix/manager-api/conf"
-)
-
-var handler *apitest.APITest
+/**
+ * Login Methods List
+ */
+const loginMethods: UserModule.LoginMethod[] = [LoginMethodPassword, LoginMethodExample];
-var (
- uriPrefix = "/apisix/admin"
-)
+/**
+ * User Logout Page
+ * @constructor
+ */
+const Page: React.FC = () => {
+ // run all logout method
+ loginMethods.forEach((item) => {
+ item.logout();
+ });
-func init() {
- //init mysql connect
- conf.InitializeMysql()
+ const redirect = getUrlQuery('redirect');
+ window.location.href = `/user/login${redirect ? `?redirect=${redirect}` : ''}`;
- r := SetUpRouter()
+ return null;
+};
- handler = apitest.New().Handler(r)
-}
+export default Page;
diff --git a/api/route/base_test.go b/src/pages/User/components/LoginMethodExample.tsx
similarity index 53%
copy from api/route/base_test.go
copy to src/pages/User/components/LoginMethodExample.tsx
index 3e3a096..305ec7a 100644
--- a/api/route/base_test.go
+++ b/src/pages/User/components/LoginMethodExample.tsx
@@ -14,25 +14,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package route
+import React from 'react';
+import { UserModule } from '@/pages/User/typing';
+import { formatMessage } from '@@/plugin-locale/localeExports';
-import (
- "github.com/api7/apitest"
+const LoginMethodExample: UserModule.LoginMethod = {
+ id: 'example',
+ name: formatMessage({ id: 'component.user.loginMethodExample' }),
+ render: () => {
+ return <a href="https://www.example.com">example</a>;
+ },
+ getData(): UserModule.LoginData {
+ return {};
+ },
+ checkData: async () => {
+ return true;
+ },
+ submit: async (data) => {
+ return {
+ status: false,
+ message: formatMessage({ id: 'component.user.loginMethodExample.message' }),
+ data,
+ };
+ },
+ logout() {},
+};
- "github.com/apisix/manager-api/conf"
-)
-
-var handler *apitest.APITest
-
-var (
- uriPrefix = "/apisix/admin"
-)
-
-func init() {
- //init mysql connect
- conf.InitializeMysql()
-
- r := SetUpRouter()
-
- handler = apitest.New().Handler(r)
-}
+export default LoginMethodExample;
diff --git a/src/pages/User/components/LoginMethodPassword.tsx b/src/pages/User/components/LoginMethodPassword.tsx
new file mode 100644
index 0000000..698addc
--- /dev/null
+++ b/src/pages/User/components/LoginMethodPassword.tsx
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { UserModule } from '@/pages/User/typing';
+import { Form, Input } from 'antd';
+import { FormInstance } from 'antd/lib/form';
+import { UserOutlined, LockTwoTone } from '@ant-design/icons';
+import { formatMessage } from '@@/plugin-locale/localeExports';
+import { request } from '@@/plugin-request/request';
+
+const formRef = React.createRef<FormInstance>();
+
+const LoginMethodPassword: UserModule.LoginMethod = {
+ id: 'password',
+ name: formatMessage({ id: 'component.user.loginMethodPassword' }),
+ render: () => {
+ return (
+ <Form ref={formRef} name="control-ref">
+ <Form.Item
+ name="username"
+ rules={[
+ {
+ required: true,
+ message: formatMessage({ id: 'component.user.loginMethodPassword.inputUsername' }),
+ },
+ ]}
+ >
+ <Input
+ size="large"
+ type="text"
+ placeholder={formatMessage({ id: 'component.user.loginMethodPassword.username' })}
+ prefix={
+ <UserOutlined
+ style={{
+ color: '#1890ff',
+ }}
+ />
+ }
+ />
+ </Form.Item>
+ <Form.Item
+ name="password"
+ rules={[
+ {
+ required: true,
+ message: formatMessage({ id: 'component.user.loginMethodPassword.inputPassword' }),
+ },
+ ]}
+ >
+ <Input
+ size="large"
+ type="password"
+ placeholder={formatMessage({ id: 'component.user.loginMethodPassword.password' })}
+ prefix={<LockTwoTone />}
+ />
+ </Form.Item>
+ </Form>
+ );
+ },
+ getData(): UserModule.LoginData {
+ if (formRef.current) {
+ const data = formRef.current.getFieldsValue();
+ return {
+ username: data.username,
+ password: data.password,
+ };
+ }
+ return {};
+ },
+ checkData: async () => {
+ if (formRef.current) {
+ try {
+ await formRef.current.validateFields();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+ return false;
+ },
+ submit: async (data) => {
+ if (data.username !== '' && data.password !== '') {
+ try {
+ const result = await request('/user/login', {
+ method: 'POST',
+ requestType: 'form',
+ prefix: 'http://localhost:8080',
+ data: {
+ username: data.username,
+ password: data.password,
+ },
+ });
+
+ localStorage.setItem('token', result.data.token);
+ return {
+ status: true,
+ message: formatMessage({ id: 'component.user.loginMethodPassword.success' }),
+ data: [],
+ };
+ } catch (e) {
+ return {
+ status: false,
+ message: formatMessage({ id: 'component.user.loginMethodPassword.incorrectPassword' }),
+ data: [],
+ };
+ }
+ } else {
+ return {
+ status: false,
+ message: formatMessage({ id: 'component.user.loginMethodPassword.fieldInvalid' }),
+ data: [],
+ };
+ }
+ },
+ logout: () => {
+ console.log('password logout');
+ localStorage.removeItem('token');
+ },
+};
+
+export default LoginMethodPassword;
diff --git a/src/pages/User/index.ts b/src/pages/User/index.ts
new file mode 100644
index 0000000..3ee57c2
--- /dev/null
+++ b/src/pages/User/index.ts
@@ -0,0 +1,2 @@
+export { default as UserZhCN } from './locales/zh-CN';
+export { default as UserEnUS } from './locales/en-US';
diff --git a/src/pages/User/locales/en-US.ts b/src/pages/User/locales/en-US.ts
new file mode 100644
index 0000000..bfcadc0
--- /dev/null
+++ b/src/pages/User/locales/en-US.ts
@@ -0,0 +1,14 @@
+export default {
+ 'component.user.login': 'Login',
+ 'component.user.loginMethodPassword': 'Username & Password',
+ 'component.user.loginMethodPassword.username': 'Username',
+ 'component.user.loginMethodPassword.password': 'Password',
+ 'component.user.loginMethodPassword.inputUsername': 'Please input username',
+ 'component.user.loginMethodPassword.inputPassword': 'Please input password',
+ 'component.user.loginMethodPassword.incorrectPassword': 'Incorrect username or password',
+ 'component.user.loginMethodPassword.fieldInvalid': 'Please check username and password',
+ 'component.user.loginMethodPassword.success': 'Login Success',
+ 'component.user.loginMethodExample': 'Example',
+ 'component.user.loginMethodExample.message':
+ 'Example Login Method, It is only used as an extension example of login method and cannot be used.',
+};
diff --git a/src/pages/User/locales/zh-CN.ts b/src/pages/User/locales/zh-CN.ts
new file mode 100644
index 0000000..d48e5bb
--- /dev/null
+++ b/src/pages/User/locales/zh-CN.ts
@@ -0,0 +1,13 @@
+export default {
+ 'component.user.login': '登录',
+ 'component.user.loginMethodPassword': '账号密码登录',
+ 'component.user.loginMethodPassword.username': '账号',
+ 'component.user.loginMethodPassword.password': '密码',
+ 'component.user.loginMethodPassword.inputUsername': '请输入账号!',
+ 'component.user.loginMethodPassword.inputPassword': '请输入密码!',
+ 'component.user.loginMethodPassword.incorrectPassword': '账号或密码错误',
+ 'component.user.loginMethodPassword.fieldInvalid': '请检查账号和密码',
+ 'component.user.loginMethodPassword.success': '登录成功',
+ 'component.user.loginMethodExample': '示例登录',
+ 'component.user.loginMethodExample.message': '示例登录方式,仅作为登录方式扩展例子,无法使用',
+};
diff --git a/src/pages/User/typing.d.ts b/src/pages/User/typing.d.ts
new file mode 100644
index 0000000..3f7d063
--- /dev/null
+++ b/src/pages/User/typing.d.ts
@@ -0,0 +1,25 @@
+import React from 'react';
+
+declare namespace UserModule {
+ interface LoginMethod {
+ id: string;
+ name: string;
+ render: () => React.ReactElement;
+ getData: () => LoginData;
+ checkData: () => Promise<boolean>;
+ submit: (data) => Promise<LoginResponse>;
+ logout: () => void;
+ }
+
+ type LoginData = {
+ [string]: string;
+ };
+
+ interface LoginResponse {
+ status: boolean;
+ message: string;
+ data: {
+ [string]: any;
+ };
+ }
+}