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) {


Reply via email to