From af283add71da71cae6a174af566075192c0afe4c Mon Sep 17 00:00:00 2001 From: Jialei Date: Mon, 30 Oct 2023 18:26:48 +0800 Subject: [PATCH] enhance(client): support login with browser (#2916) --- client/starwhale/core/instance/cli.py | 6 +-- client/starwhale/core/instance/view.py | 67 ++++++++++++++++++++++++++ console/src/i18n/locales.ts | 8 +++ console/src/pages/Auth/ClientLogin.tsx | 36 ++++++++++++++ console/src/routes.tsx | 8 +++ 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 console/src/pages/Auth/ClientLogin.tsx diff --git a/client/starwhale/core/instance/cli.py b/client/starwhale/core/instance/cli.py index 8e7da2c6b8..16f042111f 100644 --- a/client/starwhale/core/instance/cli.py +++ b/client/starwhale/core/instance/cli.py @@ -1,4 +1,3 @@ -import sys import typing as t import click @@ -42,8 +41,9 @@ def _login(instance: str, username: str, password: str, token: str, alias: str) * INSTANCE: Instance URI, if ignore it, swcli will login current selected instance. """ if not bool(password and username) ^ bool(token): - click.echo("token or password+username, only choose one type") - sys.exit(1) + click.echo("credential or token not provided, will open browser to login") + InstanceTermView().login_with_browser(instance, alias) + return if token: kw = {"token": token} diff --git a/client/starwhale/core/instance/view.py b/client/starwhale/core/instance/view.py index 6aff471a54..dac2cbc253 100644 --- a/client/starwhale/core/instance/view.py +++ b/client/starwhale/core/instance/view.py @@ -1,5 +1,9 @@ import sys +import time import typing as t +import threading +import contextlib +from urllib.parse import urlparse from rich import box from rich.panel import Panel @@ -44,6 +48,69 @@ def login(self, instance: str, alias: str, **kw: str) -> None: ) sys.exit(1) + def login_with_browser(self, instance: str, alias: str) -> None: + p = urlparse(instance) + instance = f"{p.scheme}://{p.netloc}" + + import uvicorn + from fastapi import FastAPI + from starlette.middleware.cors import CORSMiddleware + + login_done = False + + app = FastAPI() + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + @app.on_event("startup") + async def on_startup() -> None: + nonlocal instance + url = f"{instance}/auth/client" + import webbrowser + + webbrowser.open(url) + + @app.get("/login") + async def _login(token: str) -> dict: + login(instance, alias, token=token) + nonlocal login_done + login_done = True + return {"message": "success"} + + # https://github.com/encode/uvicorn/issues/742 + class Server(uvicorn.Server): + def install_signal_handlers(self) -> None: + pass + + @contextlib.contextmanager + def run_in_thread(self) -> t.Generator: + thread = threading.Thread(target=self.run) + thread.start() + try: + while not self.started: + time.sleep(1e-3) + yield + finally: + self.should_exit = True + thread.join() + + config = uvicorn.Config( + app, host="127.0.0.1", port=8007, log_level="error", loop="asyncio" + ) + server = Server(config=config) + with server.run_in_thread(): + while not login_done: + time.sleep(1e-3) + + console.print( + f":man_cook: login {instance} as [blue]{alias}[/blue] successfully!" + ) + def logout(self, instance: str) -> None: if instance == STANDALONE_INSTANCE: console.print(f":pinching_hand: skip {instance} instance logout") diff --git a/console/src/i18n/locales.ts b/console/src/i18n/locales.ts index 369c342098..7244e3e77e 100644 --- a/console/src/i18n/locales.ts +++ b/console/src/i18n/locales.ts @@ -1951,6 +1951,14 @@ const locales0 = { en: 'Wechat', zh: '微信', }, + 'Client Login Success': { + en: 'Client Login Success', + zh: '客户端登录成功', + }, + 'Client Login Failed': { + en: 'Client Login Failed', + zh: '客户端登录失败', + }, 'copyright': { en: 'Copyright © 2023 Starwhale, Inc. All rights reserved. ', zh: '版权所有 © 2023 Starwhale, Inc. 保留所有权利。', diff --git a/console/src/pages/Auth/ClientLogin.tsx b/console/src/pages/Auth/ClientLogin.tsx new file mode 100644 index 0000000000..6329611c9d --- /dev/null +++ b/console/src/pages/Auth/ClientLogin.tsx @@ -0,0 +1,36 @@ +import React, { useEffect, useState } from 'react' +import axios from 'axios' +import { cliMateServer } from '@/consts' +import { useAuth } from '@/api/Auth' +import useTranslation from '@/hooks/useTranslation' +// eslint-disable-next-line +import { Spinner } from 'baseui/spinner' + +export default function ClientLogin() { + const [t] = useTranslation() + const { token } = useAuth() + const [success, setSuccess] = useState(false) + const [loading, setLoading] = useState(false) + const [errMsg, setErrMsg] = useState('') + useEffect(() => { + if (!token) { + return + } + setLoading(true) + axios + .get(`${cliMateServer}/login`, { params: { token } }) + .then(({ data: { message } }) => { + if (message !== 'success') { + setErrMsg(message) + } else { + setSuccess(true) + } + setLoading(false) + }) + .catch(() => { + setErrMsg(t('Client Login Failed')) + setLoading(false) + }) + }, [t, token]) + return
{loading ? :
{success ? t('Client Login Success') : errMsg}
}
+} diff --git a/console/src/routes.tsx b/console/src/routes.tsx index 58a7f05410..62fe4089de 100644 --- a/console/src/routes.tsx +++ b/console/src/routes.tsx @@ -58,6 +58,7 @@ import ReportListCard from '@/pages/Report/ReportListCard' import ReportEdit from '@/pages/Report/ReportEdit' import JobOverview from './pages/Job/JobOverview' import EvaluationOverview from './pages/Evaluation/EvaluationOverview' +import ClientLogin from '@/pages/Auth/ClientLogin' const useStyles = createUseStyles({ root: ({ theme }: IThemedStyleProps) => ({ @@ -133,6 +134,13 @@ const Routes = () => { + + + + + + + {/* project */}