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