This is an automated email from the ASF dual-hosted git repository.
klesh 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 b10dea750 feat: account panel for oauth2_proxy (#5409)
b10dea750 is described below
commit b10dea750360bfa19cf0e93e761b4b4fba595008
Author: Klesh Wong <[email protected]>
AuthorDate: Fri Jun 9 09:17:25 2023 +0800
feat: account panel for oauth2_proxy (#5409)
---
backend/server/api/api.go | 7 ++
config-ui/src/App.tsx | 3 -
config-ui/src/layouts/base/api.ts | 7 ++
config-ui/src/layouts/base/base.tsx | 19 +++---
config-ui/src/pages/index.ts | 1 -
config-ui/src/pages/login/api.ts | 34 ----------
config-ui/src/pages/login/index.ts | 19 ------
config-ui/src/pages/login/login.tsx | 129 ------------------------------------
config-ui/src/pages/login/styld.ts | 39 -----------
config-ui/src/utils/request.ts | 46 ++-----------
docker-compose-dev.yml | 24 +++++++
11 files changed, 51 insertions(+), 277 deletions(-)
diff --git a/backend/server/api/api.go b/backend/server/api/api.go
index 24a017129..b0fa46290 100644
--- a/backend/server/api/api.go
+++ b/backend/server/api/api.go
@@ -67,6 +67,13 @@ func CreateApiService() {
// For both protected and unprotected routes
router.GET("/ping", ping.Get)
router.GET("/version", version.Get)
+ router.GET("/userinfo", func(ctx *gin.Context) {
+ shared.ApiOutputSuccess(ctx, gin.H{
+ "user": ctx.Request.Header.Get("X-Forwarded-User"),
+ "email":
ctx.Request.Header.Get("X-Forwarded-Email"),
+ "logoutURI": config.GetConfig().GetString("LOGOUT_URI"),
+ }, http.StatusOK)
+ })
// Endpoint to proceed database migration
router.GET("/proceed-db-migration", func(ctx *gin.Context) {
diff --git a/config-ui/src/App.tsx b/config-ui/src/App.tsx
index 1e723a1e6..8bb42ca66 100644
--- a/config-ui/src/App.tsx
+++ b/config-ui/src/App.tsx
@@ -19,7 +19,6 @@
import { Switch, Route, Redirect, Router } from 'react-router-dom';
import { ErrorLayout, BaseLayout } from '@/layouts';
import {
- LoginPage,
OfflinePage,
DBMigratePage,
ConnectionHomePage,
@@ -36,8 +35,6 @@ function App() {
return (
<Router history={history}>
<Switch>
- <Route exact path="/login" component={() => <LoginPage />} />
-
<Route
exact
path="/offline"
diff --git a/config-ui/src/layouts/base/api.ts
b/config-ui/src/layouts/base/api.ts
index d5bc56b01..c3be4bbac 100644
--- a/config-ui/src/layouts/base/api.ts
+++ b/config-ui/src/layouts/base/api.ts
@@ -18,4 +18,11 @@
import { request } from '@/utils';
+export type UserInfo = {
+ user: string;
+ email: string;
+ logoutURI: string;
+};
+
export const getVersion = () => request('/version');
+export const getUserInfo = (): Promise<UserInfo> => request('/userinfo');
diff --git a/config-ui/src/layouts/base/base.tsx
b/config-ui/src/layouts/base/base.tsx
index 104163b73..520112bb4 100644
--- a/config-ui/src/layouts/base/base.tsx
+++ b/config-ui/src/layouts/base/base.tsx
@@ -16,6 +16,7 @@
*
*/
+import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Menu, MenuItem, Tag, Navbar, Intent, Alignment, Button } from
'@blueprintjs/core';
@@ -43,7 +44,10 @@ export const BaseLayout = ({ children }: Props) => {
const { tips } = useTips();
const { ready, data } = useRefreshData<{ version: string }>(() =>
API.getVersion(), []);
- const token = window.localStorage.getItem('accessToken');
+ const [userInfo, setUserInfo] = useState<API.UserInfo | null>(null);
+ useEffect(() => {
+ API.getUserInfo().then(setUserInfo);
+ }, []);
const handlePushPath = (it: MenuItemType) => {
if (!it.target) {
@@ -53,11 +57,6 @@ 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;
@@ -149,12 +148,12 @@ export const BaseLayout = ({ children }: Props) => {
<img src={SlackIcon} alt="slack" />
<span>Slack</span>
</a>
- {token && (
+ {userInfo && userInfo.logoutURI && (
<>
<Navbar.Divider />
- <Button small intent={Intent.NONE} onClick={handleSignOut}>
- Sign Out
- </Button>
+ <span>{userInfo.email}</span>
+ <Navbar.Divider />
+ <a href={userInfo.logoutURI}>Sign Out</a>
</>
)}
</Navbar.Group>
diff --git a/config-ui/src/pages/index.ts b/config-ui/src/pages/index.ts
index 557b2860e..371eed61d 100644
--- a/config-ui/src/pages/index.ts
+++ b/config-ui/src/pages/index.ts
@@ -19,7 +19,6 @@
export * from './blueprint';
export * from './connection';
export * from './db-migrate';
-export * from './login';
export * from './offline';
export * from './pipeline';
export * from './project';
diff --git a/config-ui/src/pages/login/api.ts b/config-ui/src/pages/login/api.ts
deleted file mode 100644
index 1e36a0a46..000000000
--- a/config-ui/src/pages/login/api.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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;
-};
-
-type NewPasswordPayload = {
- username: string;
- newPassword: string;
- session: string;
-};
-
-export const login = (payload: LoginPayload) => request(`/login`, { method:
'post', data: payload });
-export const newPassword = (payload: NewPasswordPayload) =>
- request(`/login/newpassword`, { method: 'post', data: payload });
diff --git a/config-ui/src/pages/login/index.ts
b/config-ui/src/pages/login/index.ts
deleted file mode 100644
index a365b1ed2..000000000
--- a/config-ui/src/pages/login/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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.
- *
- */
-
-export * from './login';
diff --git a/config-ui/src/pages/login/login.tsx
b/config-ui/src/pages/login/login.tsx
deleted file mode 100644
index 69105f317..000000000
--- a/config-ui/src/pages/login/login.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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';
-
-const NEW_PASSWORD_REQUIRED = 'NEW_PASSWORD_REQUIRED';
-
-export const LoginPage = () => {
- const [username, setUsername] = useState(localStorage.getItem('username') ||
'');
- const [password, setPassword] = useState('');
- const [newPassword, setNewPassword] = useState('');
- const [confirmNewPassword, setConfirmNewPassword] = useState('');
- const [challenge, setChallenge] = useState('');
- const [session, setSession] = useState('');
- const loginDisabled =
- !username ||
- !password ||
- (challenge === 'NEW_PASSWORD_REQUIRED' &&
- (!newPassword || !confirmNewPassword || newPassword !==
confirmNewPassword));
-
- const history = useHistory();
-
- // () =>
- const handleSubmit = async () => {
- var request: () => Promise<any>;
-
- switch (challenge) {
- case NEW_PASSWORD_REQUIRED:
- request = () => API.newPassword({ username, session, newPassword });
- break;
- default:
- request = () => API.login({ username, password });
- break;
- }
-
- const [success, res] = await operator(request, {
- formatReason: (error) => {
- const e = error as any;
- return e?.response?.data?.causes[0];
- },
- });
- localStorage.setItem('username', username);
- if (success) {
- if (res.challengeName) {
- setChallenge(res.challengeName);
- setSession(res.session);
- } else {
- localStorage.setItem('accessToken',
res.authenticationResult.accessToken);
- localStorage.setItem('refreshToken',
res.authenticationResult.refreshToken);
- document.cookie = 'access_token=' +
res.authenticationResult.accessToken + '; path=/';
- setUsername('');
- setPassword('');
- setChallenge('');
- setSession('');
- history.push('/');
- }
- }
- };
-
- return (
- <S.Wrapper>
- <S.Inner>
- <h2>DevLake Login</h2>
- <FormGroup label="Username">
- <InputGroup
- placeholder="Username"
- value={username}
- disabled={challenge !== ''}
- onChange={(e) => setUsername((e.target as HTMLInputElement).value)}
- />
- </FormGroup>
- <FormGroup label="Password">
- <InputGroup
- type="password"
- placeholder="Password"
- value={password}
- disabled={challenge !== ''}
- onChange={(e) => setPassword((e.target as HTMLInputElement).value)}
- />
- </FormGroup>
- {challenge === 'NEW_PASSWORD_REQUIRED' && (
- <>
- <FormGroup label="Set New Password">
- <InputGroup
- type="password"
- placeholder="Please set a new Password for your account"
- value={newPassword}
- onChange={(e) => setNewPassword((e.target as
HTMLInputElement).value)}
- />
- </FormGroup>
- <FormGroup label="Confirm New Password">
- <InputGroup
- type="password"
- placeholder="Please repeat your New Password"
- value={confirmNewPassword}
- onChange={(e) => setConfirmNewPassword((e.target as
HTMLInputElement).value)}
- />
- </FormGroup>
- </>
- )}
- <Button intent={Intent.PRIMARY} onClick={handleSubmit}
disabled={loginDisabled}>
- Login
- </Button>
- </S.Inner>
- </S.Wrapper>
- );
-};
diff --git a/config-ui/src/pages/login/styld.ts
b/config-ui/src/pages/login/styld.ts
deleted file mode 100644
index e12cfadc0..000000000
--- a/config-ui/src/pages/login/styld.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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/request.ts b/config-ui/src/utils/request.ts
index b345293e1..d08301836 100644
--- a/config-ui/src/utils/request.ts
+++ b/config-ui/src/utils/request.ts
@@ -27,51 +27,11 @@ const instance = axios.create({
baseURL: DEVLAKE_ENDPOINT,
});
-var refreshingToken: Promise<any> | null = null;
-
instance.interceptors.response.use(
(response) => response,
(error) => {
const status = error.response?.status;
- if (status === 401) {
- toast.error('Please login first');
- history.push('/login');
- }
-
- if (status === 403) {
- var refreshToken = localStorage.getItem('refreshToken');
- if (refreshToken) {
- refreshingToken =
- refreshingToken ||
- request('/login/refreshtoken', {
- method: 'POST',
- data: {
- refreshToken: refreshToken,
- },
- }).then(
- (resp) => {
- localStorage.setItem('accessToken',
resp.authenticationResult.accessToken);
- refreshingToken = null;
- return resp;
- },
- (err) => {
- refreshingToken = null;
- toast.error('Please login first');
- history.push('/login');
- return Promise.reject(err);
- },
- );
- return refreshingToken.then(() => {
- const originalRequest = error.config;
- originalRequest._retry = true;
- return Promise.resolve(request(originalRequest.url,
originalRequest));
- });
- } else {
- history.push('/login');
- }
- }
-
if (status === 428) {
history.push('/db-migrate');
}
@@ -84,7 +44,8 @@ instance.interceptors.response.use(
},
);
-export type ReuqestConfig = {
+export type RequestConfig = {
+ baseURL?: string;
method?: AxiosRequestConfig['method'];
data?: unknown;
timeout?: number;
@@ -92,7 +53,7 @@ export type ReuqestConfig = {
headers?: Record<string, string>;
};
-export const request = (path: string, config?: ReuqestConfig) => {
+export const request = (path: string, config?: RequestConfig) => {
const { method = 'get', data, timeout, headers, signal } = config || {};
const cancelTokenSource = axios.CancelToken.source();
@@ -103,6 +64,7 @@ export const request = (path: string, config?:
ReuqestConfig) => {
}
const params: any = {
+ baseURL: config?.baseURL,
url: path,
method,
timeout,
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index beae68982..33235184a 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -79,6 +79,7 @@ services:
- ./logs:/app/logs
environment:
LOGGING_DIR: /app/logs
+ # LOGOUT_URI:
https://xxx.amazoncognito.com/logout?client_id=yyy&logout_uri=http%3A%2F%2Flocalhost%3A4180%2Foauth2%2Fsign_out
depends_on:
- mysql
@@ -98,6 +99,29 @@ services:
depends_on:
- devlake
+ authproxy:
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0-amd64
+ network_mode: "host"
+ ports:
+ - "4180:4180"
+ env_file:
+ - ./.env
+ # environment:
+ # OAUTH2_PROXY_PROVIDER: oidc
+ # OAUTH2_PROXY_PROVIDER_DISPLAY_NAME: my provider
+ # OAUTH2_PROXY_COOKIE_SECRET:
+ # OAUTH2_PROXY_COOKIE_DOMAINS: localhost:4180
+ # OAUTH2_PROXY_COOKIE_SECURE: 'false'
+ # OAUTH2_PROXY_EMAIL_DOMAINS: *
+ # OAUTH2_PROXY_OIDC_ISSUER_URL:
+ # OAUTH2_PROXY_OIDC_JWKS_URL:
+ # OAUTH2_PROXY_CLIENT_ID:
+ # OAUTH2_PROXY_CLIENT_SECRET:
+ # OAUTH2_PROXY_UPSTREAMS: http://localhost:4000
+ # OAUTH2_PROXY_HTTP_ADDRESS: http://0.0.0.0:4180
+ # OAUTH2_PROXY_REVERSE_PROXY: 'true'
+ # OAUTH2_PROXY_SKIP_AUTH_ROUTES: ^/grafana.*
+
volumes:
mysql-storage:
grafana-storage: