This is an automated email from the ASF dual-hosted git repository.
abeizn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 233a62bdd Grafana JWT sso (#4990)
233a62bdd is described below
commit 233a62bdd4e6f640a41177b44576252f8bf24bdb
Author: Klesh Wong <[email protected]>
AuthorDate: Sun Apr 23 18:00:54 2023 +0800
Grafana JWT sso (#4990)
* feat(framework): backend support cognito (#4926)
* feat(configui): support cognito (#4928)
* feat(config-ui): adjust the config-ui about cognito (#4933)
* feat(config-ui): hidden signout when token does not exist (#4942)
* fix(api): exclude python register from auth (#4949)
* feat: grafana sso
---------
Co-authored-by: Warren Chen <[email protected]>
Co-authored-by: 青湛 <[email protected]>
---
.env.example | 8 ++
backend/go.mod | 20 ++--
backend/go.sum | 51 +++++++--
backend/server/api/api.go | 71 ++++++++++--
backend/server/api/login/login.go | 73 ++++++++++++
backend/server/api/router.go | 6 +-
backend/server/services/auth/cognito.go | 191 ++++++++++++++++++++++++++++++++
config-ui/src/App.tsx | 61 ++++++----
config-ui/src/layouts/base/base.tsx | 21 +++-
config-ui/src/pages/login/api.ts | 26 +++++
config-ui/src/pages/login/login.tsx | 74 +++++++++++++
config-ui/src/pages/login/styld.ts | 39 +++++++
config-ui/src/utils/history.ts | 22 ++++
config-ui/src/utils/request.ts | 15 ++-
14 files changed, 622 insertions(+), 56 deletions(-)
diff --git a/.env.example b/.env.example
index 9311a30c9..ee4e1bb95 100644
--- a/.env.example
+++ b/.env.example
@@ -43,3 +43,11 @@ ENCODE_KEY=
# Set if skip verify and connect with out trusted certificate when use https
##########################
IN_SECURE_SKIP_VERIFY=
+
+
+# aws cognito
+AWS_ENABLE_COGNITO=
+AWS_AUTH_REGION=
+AWS_AUTH_USER_POOL_ID=
+AWS_AUTH_USER_POOL_WEB_CLIENT_ID=
+AWS_AUTH_COOKIE_STORAGE_DOMAIN=
diff --git a/backend/go.mod b/backend/go.mod
index 95370a7a4..34a700b92 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -4,7 +4,9 @@ go 1.19
require (
github.com/RaveNoX/go-jsonmerge v1.0.0
+ github.com/aws/aws-sdk-go v1.44.242
github.com/cockroachdb/errors v1.9.0
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.7.7
github.com/go-errors/errors v1.4.2
@@ -35,16 +37,20 @@ require (
github.com/x-cray/logrus-prefixed-formatter v0.5.2
go.temporal.io/api v1.7.1-0.20220223032354-6e6fe738916a
go.temporal.io/sdk v1.14.0
- golang.org/x/crypto v0.1.0
+ golang.org/x/crypto v0.8.0
golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
- golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
+ golang.org/x/sync v0.1.0
gorm.io/datatypes v1.0.1
gorm.io/driver/mysql v1.3.3
gorm.io/driver/postgres v1.4.5
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755
)
+require (
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+)
+
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect
@@ -121,12 +127,12 @@ require (
github.com/xanzy/ssh-agent v0.3.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/mod v0.8.0
- golang.org/x/net v0.1.0 // indirect
- golang.org/x/sys v0.1.0 // indirect
- golang.org/x/term v0.1.0 // indirect
- golang.org/x/text v0.4.0 // indirect
+ golang.org/x/net v0.9.0 // indirect
+ golang.org/x/sys v0.7.0 // indirect
+ golang.org/x/term v0.7.0 // indirect
+ golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
- golang.org/x/tools v0.2.0 // indirect
+ golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf //
indirect
google.golang.org/grpc v1.44.0 // indirect
diff --git a/backend/go.sum b/backend/go.sum
index ea1ad2764..352a9013c 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -75,6 +75,8 @@ github.com/armon/go-metrics
v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod
h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod
h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aws/aws-sdk-go v1.44.242
h1:bb6Rqd7dxh1gTUoVXLJTNC2c+zNaHpLRlNKk0kGN3fc=
+github.com/aws/aws-sdk-go v1.44.242/go.mod
h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aymerick/raymond
v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod
h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/bgentry/speakeasy v0.1.0/go.mod
h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod
h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
@@ -126,9 +128,12 @@ github.com/creack/pty v1.1.9/go.mod
h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
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/decred/dcrd/crypto/blake256 v1.0.0/go.mod
h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v4
v4.0.0-20210816181553-5444fa50b93d/go.mod
h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/denisenkom/go-mssqldb v0.9.0
h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod
h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgraph-io/badger v1.6.0/go.mod
h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
+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/go.mod
h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0/go.mod
h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -229,6 +234,9 @@ github.com/gobwas/pool v0.2.0/go.mod
h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/ws v1.0.2/go.mod
h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gocarina/gocsv v0.0.0-20220707092902-b9da1f06c77e
h1:GMIV+S6grz+vlIaUsP+fedQ6L+FovyMPMY26WO8dwQE=
github.com/gocarina/gocsv v0.0.0-20220707092902-b9da1f06c77e/go.mod
h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
+github.com/goccy/go-json v0.9.7/go.mod
h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.2
h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod
h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod
h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod
h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible
h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
@@ -439,6 +447,10 @@ github.com/jinzhu/now v1.1.2/go.mod
h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jinzhu/now v1.1.4/go.mod
h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod
h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jmespath/go-jmespath v0.4.0
h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod
h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1
h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod
h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0
h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod
h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod
h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -493,6 +505,15 @@ github.com/leodido/go-urn v1.1.0/go.mod
h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdA
github.com/leodido/go-urn v1.2.0/go.mod
h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1
h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod
h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod
h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
+github.com/lestrrat-go/blackmagic v1.0.0/go.mod
h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod
h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/iter v1.0.1/go.mod
h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
+github.com/lestrrat-go/jwx v1.2.25
h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
+github.com/lestrrat-go/jwx v1.2.25/go.mod
h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
+github.com/lestrrat-go/option v1.0.0/go.mod
h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/lestrrat-go/option v1.0.1
h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod
h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -726,6 +747,7 @@ github.com/yuin/goldmark v1.1.32/go.mod
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod
h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod
h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod
h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod
h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod
h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod
h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
@@ -779,9 +801,10 @@ golang.org/x/crypto
v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
-golang.org/x/crypto v0.1.0/go.mod
h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod
h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod
h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod
h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod
h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -819,6 +842,7 @@ golang.org/x/mod v0.3.0/go.mod
h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod
h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod
h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -870,8 +894,10 @@ golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod
h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod
h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod
h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod
h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod
h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -896,8 +922,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod
h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -970,13 +997,17 @@ golang.org/x/sys
v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod
h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod
h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod
h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod
h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -986,8 +1017,9 @@ golang.org/x/text v0.3.4/go.mod
h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod
h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod
h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod
h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1056,8 +1088,9 @@ golang.org/x/tools v0.1.1/go.mod
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod
h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod
h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
-golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
-golang.org/x/tools v0.2.0/go.mod
h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/tools v0.1.12/go.mod
h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod
h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/backend/server/api/api.go b/backend/server/api/api.go
index 27f04013e..c0d4c1f08 100644
--- a/backend/server/api/api.go
+++ b/backend/server/api/api.go
@@ -19,6 +19,10 @@ package api
import (
"fmt"
+ "github.com/apache/incubator-devlake/server/api/login"
+ "github.com/apache/incubator-devlake/server/api/ping"
+ "github.com/apache/incubator-devlake/server/api/version"
+ "github.com/apache/incubator-devlake/server/services/auth"
"net/http"
"strconv"
"strings"
@@ -54,31 +58,60 @@ Alternatively, you may downgrade back to the previous
DevLake version.
// @host localhost:8080
// @BasePath /
func CreateApiService() {
+ // Initialize services
services.Init()
+ // Get configuration
v := config.GetConfig()
+ // Set gin mode
gin.SetMode(v.GetString("MODE"))
+ // Create a gin router
router := gin.Default()
+
+ // Check if AWS Cognito is enabled
+ awsCognitoEnabled := v.GetBool("AWS_ENABLE_COGNITO")
+
+ // For both protected and unprotected routes
+ router.GET("/ping", ping.Get)
+ router.GET("/version", version.Get)
+ // Check if remote plugins are enabled
remotePluginsEnabled := v.GetBool("ENABLE_REMOTE_PLUGINS")
if remotePluginsEnabled {
+ // Add endpoint to register remote plugins
router.POST("/plugins/register", remote.RegisterPlugin(router,
registerPluginEndpoints))
}
- // Wait for user confirmation if db migration is needed
+
+ if awsCognitoEnabled {
+ // Add login endpoint
+ router.POST("/login", login.Login)
+ // Use AuthenticationMiddleware for protected routes
+ router.Use(auth.AuthenticationMiddleware)
+ }
+
+ // Endpoint to proceed database migration
router.GET("/proceed-db-migration", func(ctx *gin.Context) {
+ // Check if migration requires confirmation
if !services.MigrationRequireConfirmation() {
+ // Return success response
shared.ApiOutputSuccess(ctx, nil, http.StatusOK)
return
}
+ // Execute database migration
err := services.ExecuteMigration()
if err != nil {
+ // Return error response
shared.ApiOutputError(ctx, errors.Default.Wrap(err,
"error executing migration"))
return
}
+ // Return success response
shared.ApiOutputSuccess(ctx, nil, http.StatusOK)
})
+
+ // Restrict access if database migration is required
router.Use(func(ctx *gin.Context) {
if !services.MigrationRequireConfirmation() {
return
}
+ // Return error response
shared.ApiOutputError(
ctx,
errors.HttpStatus(http.StatusPreconditionRequired).New(DB_MIGRATION_REQUIRED),
@@ -86,34 +119,47 @@ func CreateApiService() {
ctx.Abort()
})
+ // Add swagger handler
router.GET("/swagger/*any",
ginSwagger.WrapHandler(swaggerFiles.Handler))
- //endpoint debug log
+ // Add debug logging for endpoints
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName
string, nuHandlers int) {
logruslog.Global.Printf("endpoint %v %v %v %v", httpMethod,
absolutePath, handlerName, nuHandlers)
}
- // CORS CONFIG
+ // Enable CORS
router.Use(cors.New(cors.Config{
- AllowOrigins: []string{"*"},
- AllowMethods: []string{"PUT", "PATCH", "POST", "GET",
"OPTIONS"},
- AllowHeaders: []string{"Origin", "Content-Type"},
- ExposeHeaders: []string{"Content-Length"},
+ // Allow all origins
+ AllowOrigins: []string{"*"},
+ // Allow common methods
+ AllowMethods: []string{"PUT", "PATCH", "POST", "GET",
"OPTIONS"},
+ // Allow common headers
+ AllowHeaders: []string{"Origin", "Content-Type"},
+ // Expose these headers
+ ExposeHeaders: []string{"Content-Length"},
+ // Allow credentials
AllowCredentials: true,
- MaxAge: 120 * time.Hour,
+ // Cache for 2 hours
+ MaxAge: 120 * time.Hour,
}))
+ // Register API endpoints
RegisterRouter(router)
- port := v.GetString("PORT")
- port = strings.TrimLeft(port, ":")
if remotePluginsEnabled {
go bootstrapRemotePlugins(v)
}
+ // Get port from config
+ port := v.GetString("PORT")
+ // Trim any : from the start
+ port = strings.TrimLeft(port, ":")
+ // Convert to int
portNum, err := strconv.Atoi(port)
if err != nil {
+ // Panic if PORT is not an int
panic(fmt.Errorf("PORT [%s] must be int: %s", port,
err.Error()))
}
+ // Start the server
err = router.Run(fmt.Sprintf("0.0.0.0:%d", portNum))
if err != nil {
panic(err)
@@ -121,12 +167,17 @@ func CreateApiService() {
}
func bootstrapRemotePlugins(v *viper.Viper) {
+ // Get port from config
port := v.GetString("PORT")
+ // Trim any : from the start
port = strings.TrimLeft(port, ":")
+ // Convert to int
portNum, err := strconv.Atoi(port)
if err != nil {
+ // Panic if PORT is not an int
panic(fmt.Errorf("PORT [%s] must be int: %s", port,
err.Error()))
}
+ // Bootstrap remote plugins
err = bridge.Bootstrap(v, portNum)
if err != nil {
logruslog.Global.Error(err, "")
diff --git a/backend/server/api/login/login.go
b/backend/server/api/login/login.go
new file mode 100644
index 000000000..7655529c8
--- /dev/null
+++ b/backend/server/api/login/login.go
@@ -0,0 +1,73 @@
+/*
+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 login
+
+import (
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/server/api/shared"
+ "github.com/apache/incubator-devlake/server/services/auth"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+type LoginRequest struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
+
+type LoginResponse struct {
+ AuthenticationResult AuthenticationResult `json:"AuthenticationResult"`
+ ChallengeName interface{} `json:"ChallengeName"`
+ ChallengeParameters ChallengeParameters `json:"ChallengeParameters"`
+ Session interface{} `json:"Session"`
+}
+type AuthenticationResult struct {
+ AccessToken string `json:"AccessToken"`
+ ExpiresIn int `json:"ExpiresIn"`
+ IDToken string `json:"IdToken"`
+ NewDeviceMetadata interface{} `json:"NewDeviceMetadata"`
+ RefreshToken string `json:"RefreshToken"`
+ TokenType string `json:"TokenType"`
+}
+type ChallengeParameters struct {
+}
+
+// @Summary post login
+// @Description post login
+// @Tags framework/login
+// @Accept application/json
+// @Param blueprint body LoginRequest true "json"
+// @Success 200 {object} LoginResponse
+// @Failure 400 {object} shared.ApiBody "Bad Request"
+// @Failure 500 {object} shared.ApiBody "Internal Error"
+// @Router /login [post]
+func Login(ctx *gin.Context) {
+ loginReq := &LoginRequest{}
+ err := ctx.ShouldBind(loginReq)
+ if err != nil {
+ shared.ApiOutputError(ctx, errors.BadInput.Wrap(err,
shared.BadRequestBody))
+ return
+ }
+ res, err := auth.SignIn(auth.CreateCognitoClient(), loginReq.Username,
loginReq.Password)
+ if err != nil {
+ shared.ApiOutputError(ctx, errors.Default.Wrap(err, "error
signing in"))
+ return
+ }
+ shared.ApiOutputSuccess(ctx, res, http.StatusOK)
+}
diff --git a/backend/server/api/router.go b/backend/server/api/router.go
index 8408a7af0..26114da9b 100644
--- a/backend/server/api/router.go
+++ b/backend/server/api/router.go
@@ -25,14 +25,12 @@ import (
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/server/api/blueprints"
"github.com/apache/incubator-devlake/server/api/domainlayer"
- "github.com/apache/incubator-devlake/server/api/ping"
"github.com/apache/incubator-devlake/server/api/pipelines"
"github.com/apache/incubator-devlake/server/api/plugininfo"
"github.com/apache/incubator-devlake/server/api/project"
"github.com/apache/incubator-devlake/server/api/push"
"github.com/apache/incubator-devlake/server/api/shared"
"github.com/apache/incubator-devlake/server/api/task"
- "github.com/apache/incubator-devlake/server/api/version"
"github.com/apache/incubator-devlake/server/services"
"github.com/gin-gonic/gin"
@@ -57,8 +55,8 @@ func RegisterRouter(r *gin.Engine) {
r.GET("/pipelines/:pipelineId/logging.tar.gz", pipelines.DownloadLogs)
- r.GET("/ping", ping.Get)
- r.GET("/version", version.Get)
+ //r.GET("/ping", ping.Get)
+ //r.GET("/version", version.Get)
r.POST("/push/:tableName", push.Post)
r.GET("/domainlayer/repos", domainlayer.ReposIndex)
diff --git a/backend/server/services/auth/cognito.go
b/backend/server/services/auth/cognito.go
new file mode 100644
index 000000000..d2872b13a
--- /dev/null
+++ b/backend/server/services/auth/cognito.go
@@ -0,0 +1,191 @@
+/*
+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 auth
+
+import (
+ "crypto/rsa"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math/big"
+ "net/http"
+ "strings"
+ "sync"
+
+ "github.com/apache/incubator-devlake/core/config"
+ "github.com/apache/incubator-devlake/impls/logruslog"
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/gin-gonic/gin"
+)
+
+var (
+ //jwksCache is a cache of the fetched JWKS
+ jwksCache Jwks
+ //jwksCacheMx is a mutex to lock the jwksCache
+ jwksCacheMx sync.Mutex
+ logger = logruslog.Global.Nested("auth")
+)
+
+func CreateCognitoClient() *cognitoidentityprovider.CognitoIdentityProvider {
+ // Get configuration
+ v := config.GetConfig()
+ // Create an AWS session
+ sess := session.Must(session.NewSession(&aws.Config{
+ Region: aws.String(v.GetString("AWS_AUTH_REGION")),
+ }))
+ // Create a Cognito Identity Provider client
+ return cognitoidentityprovider.New(sess)
+}
+
+func SignIn(cognitoClient *cognitoidentityprovider.CognitoIdentityProvider,
username, password string) (*cognitoidentityprovider.InitiateAuthOutput, error)
{
+ // Get configuration
+ v := config.GetConfig()
+ // Create the input for InitiateAuth
+ input := &cognitoidentityprovider.InitiateAuthInput{
+ AuthFlow: aws.String("USER_PASSWORD_AUTH"),
+ ClientId:
aws.String(v.GetString("AWS_AUTH_USER_POOL_WEB_CLIENT_ID")),
+ AuthParameters: map[string]*string{
+ "USERNAME": aws.String(username),
+ "PASSWORD": aws.String(password),
+ },
+ }
+
+ // Call Cognito to get auth tokens
+ response, err := cognitoClient.InitiateAuth(input)
+ if err != nil {
+ return nil, err
+ }
+ return response, nil
+}
+
+func fetchJWKS(jwksURL string) (jwks Jwks, err error) {
+ // Get the JWKS from the URL
+ resp, err := http.Get(jwksURL)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return
+ }
+ // Unmarshal the response into a Jwks struct
+ err = json.Unmarshal(body, &jwks)
+ return
+}
+
+func ensureJWKS(jwksURL string) (jwks Jwks, err error) {
+ // Lock the mutex
+ jwksCacheMx.Lock()
+ defer jwksCacheMx.Unlock()
+
+ // If the cache is empty, fetch the JWKS
+ if len(jwksCache.Keys) == 0 {
+ jwksCache, err = fetchJWKS(jwksURL)
+ }
+ // Return the cached JWKS
+ jwks = jwksCache
+ return
+}
+
+func AuthenticationMiddleware(ctx *gin.Context) {
+ // Get configuration
+ v := config.GetConfig()
+ // Construct the JWKS URL
+ jwksURL :=
fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json",
v.GetString("AWS_AUTH_REGION"), v.GetString("AWS_AUTH_USER_POOL_ID"))
+ // Get the cached JWKS
+ jwks, err := ensureJWKS(jwksURL)
+ if err != nil {
+ fmt.Printf("Error fetching JWKS: %v\n", err)
+ ctx.Abort()
+ return
+ }
+
+ // Get the Auth header
+ authHeader := ctx.GetHeader("Authorization")
+ if authHeader == "" {
+ http.Error(ctx.Writer, "Authorization header is missing",
http.StatusUnauthorized)
+ ctx.Abort()
+ return
+ }
+
+ // Split the header into "Bearer" and the actual token
+ bearerToken := strings.Split(authHeader, " ")
+ if len(bearerToken) != 2 {
+ http.Error(ctx.Writer, "Invalid Authorization header",
http.StatusUnauthorized)
+ ctx.Abort()
+ return
+ }
+
+ // Parse the JWT token
+ token, err := jwt.Parse(bearerToken[1], func(token *jwt.Token)
(interface{}, error) {
+ // Check the signing method
+ if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
+ return nil, fmt.Errorf("Unexpected signing method: %v",
token.Header["alg"])
+ }
+
+ // Get the key ID from the header
+ kid := token.Header["kid"].(string)
+
+ // Look for the key that matches the kid
+ for _, key := range jwks.Keys {
+ if key.Kid == kid {
+ // Construct the RSA public key
+ n := pemHeader(key.N)
+ e := pemHeader(key.E)
+ parsedKey := &rsa.PublicKey{
+ N: new(big.Int).SetBytes(n),
+ E:
int(new(big.Int).SetBytes(e).Int64()),
+ }
+ return parsedKey, nil
+ }
+ }
+
+ return nil, fmt.Errorf("Public key not found")
+ })
+
+ // Check if the token is invalid
+ if err != nil || !token.Valid {
+ logger.Error(err, "Invalid token")
+ http.Error(ctx.Writer, "Invalid token", http.StatusUnauthorized)
+ ctx.Abort()
+ }
+}
+
+func pemHeader(encodedKey string) []byte {
+ // Decode the base64 encoded key
+ key, err := base64.RawURLEncoding.DecodeString(encodedKey)
+ if err != nil {
+ return nil
+ }
+ return key
+}
+
+// Jwks represents the JSON web key set
+type Jwks struct {
+ Keys []struct {
+ Kid string `json:"kid"`
+ N string `json:"n"`
+ E string `json:"e"`
+ } `json:"keys"`
+}
diff --git a/config-ui/src/App.tsx b/config-ui/src/App.tsx
index ea9b4e969..f655349a5 100644
--- a/config-ui/src/App.tsx
+++ b/config-ui/src/App.tsx
@@ -16,8 +16,9 @@
*
*/
-import { Switch, Route, Redirect } from 'react-router-dom';
-
+import { Switch, Route, Redirect, Router } from 'react-router-dom';
+import { LoginPage } from './pages/login/login';
+import { history } from './utils/history';
import { BaseLayout } from '@/layouts';
import { FromEnum } from '@/pages';
import {
@@ -35,29 +36,47 @@ import {
function App() {
return (
- <BaseLayout>
+ <Router history={history}>
<Switch>
- <Route path="/" exact component={() => <Redirect to="/projects" />} />
- <Route exact path="/projects" component={() => <ProjectHomePage />} />
- <Route exact path="/projects/:pname" component={() =>
<ProjectDetailPage />} />
- <Route exact path="/projects/:pname/:bid/connection-add" component={()
=> <BlueprintConnectioAddPage />} />
- <Route exact path="/projects/:pname/:bid/:unique" component={() =>
<BlueprintConnectionDetailPage />} />
+ <Route exact path="/login" component={() => <LoginPage />} />
<Route
- exact
- path="/projects/:pname/create-blueprint"
- component={() => <BlueprintCreatePage from={FromEnum.project} />}
+ path="/"
+ component={() => (
+ <BaseLayout>
+ <Switch>
+ <Route exact path="/" component={() => <Redirect
to="/projects" />} />
+ <Route exact path="/projects" component={() =>
<ProjectHomePage />} />
+ <Route exact path="/projects/:pname" component={() =>
<ProjectDetailPage />} />
+ <Route
+ exact
+ path="/projects/:pname/:bid/connection-add"
+ component={() => <BlueprintConnectioAddPage />}
+ />
+ <Route exact path="/projects/:pname/:bid/:unique"
component={() => <BlueprintConnectionDetailPage />} />
+ <Route
+ exact
+ path="/projects/:pname/create-blueprint"
+ component={() => <BlueprintCreatePage
from={FromEnum.project} />}
+ />
+ <Route exact path="/connections" component={() =>
<ConnectionHomePage />} />
+ <Route exact path="/connections/:plugin" component={() =>
<ConnectionListPage />} />
+ <Route exact path="/connections/:plugin/create" component={()
=> <ConnectionFormPage />} />
+ <Route exact path="/connections/:plugin/:cid" component={() =>
<ConnectionFormPage />} />
+ <Route exact path="/blueprints" component={() =>
<BlueprintHomePage />} />
+ <Route
+ exact
+ path="/blueprints/create"
+ component={() => <BlueprintCreatePage
from={FromEnum.blueprint} />}
+ />
+ <Route exact path="/blueprints/:id" component={() =>
<BlueprintDetailPage />} />
+ <Route exact path="/blueprints/:bid/connection-add"
component={() => <BlueprintConnectioAddPage />} />
+ <Route exact path="/blueprints/:bid/:unique" component={() =>
<BlueprintConnectionDetailPage />} />
+ </Switch>
+ </BaseLayout>
+ )}
/>
- <Route exact path="/connections" component={() => <ConnectionHomePage
/>} />
- <Route exact path="/connections/:plugin" component={() =>
<ConnectionListPage />} />
- <Route exact path="/connections/:plugin/create" component={() =>
<ConnectionFormPage />} />
- <Route exact path="/connections/:plugin/:cid" component={() =>
<ConnectionFormPage />} />
- <Route exact path="/blueprints" component={() => <BlueprintHomePage
/>} />
- <Route exact path="/blueprints/create" component={() =>
<BlueprintCreatePage from={FromEnum.blueprint} />} />
- <Route exact path="/blueprints/:id" component={() =>
<BlueprintDetailPage />} />
- <Route exact path="/blueprints/:bid/connection-add" component={() =>
<BlueprintConnectioAddPage />} />
- <Route exact path="/blueprints/:bid/:unique" component={() =>
<BlueprintConnectionDetailPage />} />
</Switch>
- </BaseLayout>
+ </Router>
);
}
diff --git a/config-ui/src/layouts/base/base.tsx
b/config-ui/src/layouts/base/base.tsx
index 5716f8c59..5f546a458 100644
--- a/config-ui/src/layouts/base/base.tsx
+++ b/config-ui/src/layouts/base/base.tsx
@@ -17,11 +17,12 @@
*/
import React from 'react';
-import { useLocation, useHistory } from 'react-router-dom';
-import { Menu, MenuItem, Tag, Navbar, Intent, Alignment } from
'@blueprintjs/core';
+import { useLocation } from 'react-router-dom';
+import { Menu, MenuItem, Tag, Navbar, Intent, Alignment, Button } from
'@blueprintjs/core';
import { Logo, ExternalLink } from '@/components';
import { useVersion } from '@/store';
+import { history } from '@/utils/history';
import DashboardIcon from '@/images/icons/dashborad.svg';
import FileIcon from '@/images/icons/file.svg';
@@ -38,9 +39,10 @@ interface Props {
export const BaseLayout = ({ children }: Props) => {
const menu = useMenu();
const { pathname } = useLocation();
- const history = useHistory();
const { version } = useVersion();
+ const token = window.localStorage.getItem('accessToken');
+
const handlePushPath = (it: MenuItemType) => {
if (!it.target) {
history.push(it.path);
@@ -49,6 +51,11 @@ export const BaseLayout = ({ children }: Props) => {
}
};
+ const handleSignOut = () => {
+ localStorage.removeItem(`accessToken`);
+ history.push('/login');
+ };
+
const getGrafanaUrl = () => {
const suffix = '/d/lCO8w-pVk/homepage?orgId=1';
const { protocol, hostname } = window.location;
@@ -131,6 +138,14 @@ export const BaseLayout = ({ children }: Props) => {
<img src={SlackIcon} alt="slack" />
<span>Slack</span>
</a>
+ {token && (
+ <>
+ <Navbar.Divider />
+ <Button small intent={Intent.NONE} onClick={handleSignOut}>
+ Sign Out
+ </Button>
+ </>
+ )}
</Navbar.Group>
</S.Header>
<S.Inner>
diff --git a/config-ui/src/pages/login/api.ts b/config-ui/src/pages/login/api.ts
new file mode 100644
index 000000000..c5a749309
--- /dev/null
+++ b/config-ui/src/pages/login/api.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { request } from '@/utils';
+
+type LoginPayload = {
+ username: string;
+ password: string;
+};
+
+export const login = (payload: LoginPayload) => request(`/login`, { method:
'post', data: payload });
diff --git a/config-ui/src/pages/login/login.tsx
b/config-ui/src/pages/login/login.tsx
new file mode 100644
index 000000000..a45cc7f0e
--- /dev/null
+++ b/config-ui/src/pages/login/login.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { useState } from 'react';
+import { useHistory } from 'react-router-dom';
+import { FormGroup, InputGroup, Button, Intent } from '@blueprintjs/core';
+
+import { operator } from '@/utils';
+
+import * as API from './api';
+import * as S from './styld';
+
+export const LoginPage = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+
+ const history = useHistory();
+
+ const handleSubmit = async () => {
+ const [success, res] = await operator(() => API.login({ username, password
}), {
+ formatReason: (error) => 'Login failed',
+ });
+
+ if (success) {
+ localStorage.setItem('accessToken',
res.AuthenticationResult.AccessToken);
+ document.cookie = 'access_token=' + res.AuthenticationResult.AccessToken
+ '; path=/';
+ history.push('/');
+ }
+
+ setUsername('');
+ setPassword('');
+ };
+
+ return (
+ <S.Wrapper>
+ <S.Inner>
+ <h2>DevLake Login</h2>
+ <FormGroup label="Username">
+ <InputGroup
+ placeholder="Username"
+ value={username}
+ onChange={(e) => setUsername((e.target as HTMLInputElement).value)}
+ />
+ </FormGroup>
+ <FormGroup label="Password">
+ <InputGroup
+ type="password"
+ placeholder="Password"
+ value={password}
+ onChange={(e) => setPassword((e.target as HTMLInputElement).value)}
+ />
+ </FormGroup>
+ <Button intent={Intent.PRIMARY} onClick={handleSubmit}>
+ Login
+ </Button>
+ </S.Inner>
+ </S.Wrapper>
+ );
+};
diff --git a/config-ui/src/pages/login/styld.ts
b/config-ui/src/pages/login/styld.ts
new file mode 100644
index 000000000..e12cfadc0
--- /dev/null
+++ b/config-ui/src/pages/login/styld.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 styled from 'styled-components';
+
+export const Wrapper = styled.div`
+ height: 100vh;
+ background-color: #f5f5f5;
+ overflow: hidden;
+`;
+
+export const Inner = styled.div`
+ margin: 200px auto;
+ padding: 16px 24px;
+ width: 400px;
+ background-color: #fff;
+ border-raidus: 4px;
+ box-shadow: 0px 2.4px 4.8px -0.8px rgba(0, 0, 0, 0.1), 0px 1.6px 8px rgba(0,
0, 0, 0.07);
+
+ h2 {
+ margin-bottom: 16px;
+ text-align: center;
+ }
+`;
diff --git a/config-ui/src/utils/history.ts b/config-ui/src/utils/history.ts
new file mode 100644
index 000000000..2b705cfd3
--- /dev/null
+++ b/config-ui/src/utils/history.ts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ *
+ */
+
+// history.js or history.ts
+import { createBrowserHistory } from 'history';
+
+export const history = createBrowserHistory();
diff --git a/config-ui/src/utils/request.ts b/config-ui/src/utils/request.ts
index 450b5421f..2fb8ba59b 100644
--- a/config-ui/src/utils/request.ts
+++ b/config-ui/src/utils/request.ts
@@ -18,8 +18,10 @@
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
+import { history } from '@/utils/history';
import { DEVLAKE_ENDPOINT } from '@/config';
+import { toast } from '@/components/toast';
const instance = axios.create({
baseURL: DEVLAKE_ENDPOINT,
@@ -35,13 +37,12 @@ export type ReuqestConfig = {
export const request = (path: string, config?: ReuqestConfig) => {
const { method = 'get', data, timeout, headers, signal } = config || {};
-
const cancelTokenSource = axios.CancelToken.source();
const params: any = {
url: path,
method,
timeout,
- headers,
+ headers: { ...headers, Authorization: `Bearer
${localStorage.getItem('accessToken')}` },
cancelToken: cancelTokenSource?.token,
};
@@ -51,6 +52,16 @@ export const request = (path: string, config?:
ReuqestConfig) => {
params.data = data;
}
+ instance.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response && error.response.status === 401) {
+ toast.error('Please log in first');
+ history.push('/login');
+ }
+ },
+ );
+
const promise = instance.request(params).then((resp) => resp.data);
if (signal) {