This is an automated email from the ASF dual-hosted git repository.

warren pushed a commit to branch cloud-service-be-cognito
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/cloud-service-be-cognito by 
this push:
     new b208d75ad feat(configui): support cognito (#4928)
b208d75ad is described below

commit b208d75ad3558f01220c2a2d2ee0d109cf21c288
Author: Warren Chen <[email protected]>
AuthorDate: Fri Apr 14 15:17:46 2023 +0800

    feat(configui): support cognito (#4928)
---
 backend/server/api/api.go                     | 10 +++-
 backend/server/api/login/login.go             |  4 +-
 backend/server/api/router.go                  |  6 +--
 backend/server/services/{ => auth}/cognito.go |  2 +-
 config-ui/src/App.tsx                         | 54 +++++++++++----------
 config-ui/src/layouts/base/base.tsx           | 17 +++++--
 config-ui/src/pages/login/login.tsx           | 68 +++++++++++++++++++++++++++
 config-ui/src/utils/history.ts                | 22 +++++++++
 config-ui/src/utils/request.ts                | 14 +++++-
 9 files changed, 159 insertions(+), 38 deletions(-)

diff --git a/backend/server/api/api.go b/backend/server/api/api.go
index b444d1e6e..7f09f5669 100644
--- a/backend/server/api/api.go
+++ b/backend/server/api/api.go
@@ -20,6 +20,9 @@ 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"
@@ -66,11 +69,16 @@ func CreateApiService() {
 
        // 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)
+
        if awsCognitoEnabled {
                // Add login endpoint
                router.POST("/login", login.Login)
                // Use AuthenticationMiddleware for protected routes
-               router.Use(services.AuthenticationMiddleware)
+               router.Use(auth.AuthenticationMiddleware)
        }
 
        // Check if remote plugins are enabled
diff --git a/backend/server/api/login/login.go 
b/backend/server/api/login/login.go
index 69a5c44aa..7655529c8 100644
--- a/backend/server/api/login/login.go
+++ b/backend/server/api/login/login.go
@@ -20,7 +20,7 @@ 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"
+       "github.com/apache/incubator-devlake/server/services/auth"
        "net/http"
 
        "github.com/gin-gonic/gin"
@@ -64,7 +64,7 @@ func Login(ctx *gin.Context) {
                shared.ApiOutputError(ctx, errors.BadInput.Wrap(err, 
shared.BadRequestBody))
                return
        }
-       res, err := services.SignIn(services.CreateCognitoClient(), 
loginReq.Username, loginReq.Password)
+       res, err := auth.SignIn(auth.CreateCognitoClient(), loginReq.Username, 
loginReq.Password)
        if err != nil {
                shared.ApiOutputError(ctx, errors.Default.Wrap(err, "error 
signing in"))
                return
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/cognito.go 
b/backend/server/services/auth/cognito.go
similarity index 99%
rename from backend/server/services/cognito.go
rename to backend/server/services/auth/cognito.go
index 8907323a4..87018e990 100644
--- a/backend/server/services/cognito.go
+++ b/backend/server/services/auth/cognito.go
@@ -15,7 +15,7 @@ See the License for the specific language governing 
permissions and
 limitations under the License.
 */
 
-package services
+package auth
 
 import (
        "crypto/rsa"
diff --git a/config-ui/src/App.tsx b/config-ui/src/App.tsx
index ea9b4e969..5f5299cb6 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,32 @@ import {
 
 function App() {
   return (
-    <BaseLayout>
-      <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="/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>
+    <Router history={history}>
+      <BaseLayout>
+        <Switch>
+          <Route path="/" exact component={() => <Redirect to="/projects" />} 
/>
+          <Route exact path="/login" component={() => <LoginPage />} />
+          <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>
+    </Router>
   );
 }
 
diff --git a/config-ui/src/layouts/base/base.tsx 
b/config-ui/src/layouts/base/base.tsx
index 5716f8c59..2534b8ac7 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,7 +39,6 @@ interface Props {
 export const BaseLayout = ({ children }: Props) => {
   const menu = useMenu();
   const { pathname } = useLocation();
-  const history = useHistory();
   const { version } = useVersion();
 
   const handlePushPath = (it: MenuItemType) => {
@@ -131,6 +131,10 @@ export const BaseLayout = ({ children }: Props) => {
               <img src={SlackIcon} alt="slack" />
               <span>Slack</span>
             </a>
+            <Navbar.Divider />
+            <Button intent={Intent.NONE} onClick={handleSignOut}>
+              Sign Out
+            </Button>
           </Navbar.Group>
         </S.Header>
         <S.Inner>
@@ -140,3 +144,10 @@ export const BaseLayout = ({ children }: Props) => {
     </S.Wrapper>
   );
 };
+
+const handleSignOut = () => {
+  // Clear user authentication information (e.g. token)
+  localStorage.removeItem(`accessToken`);
+  // Redirect user to the login page
+  history.push('/login');
+};
diff --git a/config-ui/src/pages/login/login.tsx 
b/config-ui/src/pages/login/login.tsx
new file mode 100644
index 000000000..cf4d8e95a
--- /dev/null
+++ b/config-ui/src/pages/login/login.tsx
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { request } from '@/utils';
+import React, { useState, ChangeEvent, FormEvent } from 'react';
+import { useHistory } from 'react-router-dom';
+
+export const LoginPage = () => {
+  const [username, setUsername] = useState<string>('');
+  const [password, setPassword] = useState<string>('');
+  const history = useHistory();
+
+  const handleSubmit = (e: FormEvent) => {
+    e.preventDefault();
+    const payload = { username, password };
+    try {
+      login(payload).then((r) => {
+        localStorage.setItem('accessToken', 
r.AuthenticationResult.AccessToken);
+        history.push('/projects');
+      });
+    } catch (error) {
+      console.error('Error during login:', error);
+      // Handle login error here, e.g. show a message to the user
+    }
+  };
+
+  return (
+    <div>
+      <h2>"login"</h2>
+      <form onSubmit={handleSubmit}>
+        <div>
+          <label>Username:</label>
+          <input
+            type="text"
+            value={username}
+            onChange={(e: ChangeEvent<HTMLInputElement>) => 
setUsername(e.target.value)}
+          />
+        </div>
+        <div>
+          <label>Password:</label>
+          <input
+            type="password"
+            value={password}
+            onChange={(e: ChangeEvent<HTMLInputElement>) => 
setPassword(e.target.value)}
+          />
+        </div>
+        <button type="submit">login</button>
+      </form>
+    </div>
+  );
+};
+
+export const login = (payload: any) => request(`/login`, { method: 'post', 
data: payload });
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..5b66b77af 100644
--- a/config-ui/src/utils/request.ts
+++ b/config-ui/src/utils/request.ts
@@ -18,6 +18,7 @@
 
 import type { AxiosRequestConfig } from 'axios';
 import axios from 'axios';
+import { history } from '@/utils/history';
 
 import { DEVLAKE_ENDPOINT } from '@/config';
 
@@ -35,13 +36,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 +51,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) {
+        console.log('401 error');
+        history.push('/login');
+      }
+    },
+  );
+
   const promise = instance.request(params).then((resp) => resp.data);
 
   if (signal) {

Reply via email to