Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(client): support login with browser #2916

Merged
merged 4 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/starwhale/core/instance/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
import typing as t

import click
Expand Down Expand Up @@ -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}
Expand Down
66 changes: 66 additions & 0 deletions client/starwhale/core/instance/view.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -44,6 +48,68 @@ 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:
print(f"get token {token}")
goldenxinxing marked this conversation as resolved.
Show resolved Hide resolved
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)

print(f"login {instance} as {alias} successfully!")

def logout(self, instance: str) -> None:
if instance == STANDALONE_INSTANCE:
console.print(f":pinching_hand: skip {instance} instance logout")
Expand Down
8 changes: 8 additions & 0 deletions console/src/i18n/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. 保留所有权利。',
Expand Down
35 changes: 35 additions & 0 deletions console/src/pages/Auth/ClientLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { useEffect, useState } from 'react'
import axios from 'axios'
import { cliMateServer } from '@/consts'
import { useAuth } from '@/api/Auth'
import useTranslation from '@/hooks/useTranslation'
import { Spinner } from 'baseui/spinner'

Check warning on line 6 in console/src/pages/Auth/ClientLogin.tsx

View workflow job for this annotation

GitHub Actions / build (16.x)

The "Spinner" component has been deprecated in favor of "StyledSpinnerNext". In v11 of baseui, "Spinner" will be removed and "StyledSpinnerNext" will be renamed to "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
goldenxinxing marked this conversation as resolved.
Show resolved Hide resolved
}
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)
})
}, [])

Check warning on line 33 in console/src/pages/Auth/ClientLogin.tsx

View workflow job for this annotation

GitHub Actions / build (16.x)

React Hook useEffect has missing dependencies: 't' and 'token'. Either include them or remove the dependency array
return <div>{loading ? <Spinner /> : <div>{success ? t('Client Login Success') : errMsg}</div>}</div>
}
8 changes: 8 additions & 0 deletions console/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down Expand Up @@ -133,6 +134,13 @@ const Routes = () => {
</Switch>
</SettingsOverviewLayout>
</Route>
<Route exact path='/auth/:path?'>
<CenterLayout>
<Switch>
<Route exact path='/auth/client' component={ClientLogin} />
</Switch>
</CenterLayout>
</Route>
{/* project */}
<Route exact path='/projects/:projectId/members'>
<CenterLayout>
Expand Down
Loading