Show device token while signing in (#3935)
* Show user code while logging in
* fmt
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)
* @jtran feedback
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"
This reverts commit 5ba9e4351a
.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
5
interface.d.ts
vendored
5
interface.d.ts
vendored
@ -10,7 +10,10 @@ export interface IElectronAPI {
|
|||||||
save: typeof dialog.showSaveDialog
|
save: typeof dialog.showSaveDialog
|
||||||
openExternal: typeof shell.openExternal
|
openExternal: typeof shell.openExternal
|
||||||
showInFolder: typeof shell.showItemInFolder
|
showInFolder: typeof shell.showItemInFolder
|
||||||
login: (host: string) => Promise<string>
|
/** Require to be called first before {@link loginWithDeviceFlow} */
|
||||||
|
startDeviceFlow: (host: string) => Promise<string>
|
||||||
|
/** Registered by first calling {@link startDeviceFlow}, which sets up the device flow handle */
|
||||||
|
loginWithDeviceFlow: () => Promise<string>
|
||||||
platform: typeof process.env.platform
|
platform: typeof process.env.platform
|
||||||
arch: typeof process.env.arch
|
arch: typeof process.env.arch
|
||||||
version: typeof process.env.version
|
version: typeof process.env.version
|
||||||
|
40
src/main.ts
40
src/main.ts
@ -160,7 +160,7 @@ ipcMain.handle('shell.openExternal', (event, data) => {
|
|||||||
return shell.openExternal(data)
|
return shell.openExternal(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('login', async (event, host) => {
|
ipcMain.handle('startDeviceFlow', async (_, host: string) => {
|
||||||
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
||||||
// We quiet ts because we are not using this in the standard way.
|
// We quiet ts because we are not using this in the standard way.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -178,21 +178,33 @@ ipcMain.handle('login', async (event, host) => {
|
|||||||
|
|
||||||
const handle = await client.deviceAuthorization()
|
const handle = await client.deviceAuthorization()
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// Register this handle to be used later.
|
||||||
shell.openExternal(handle.verification_uri_complete)
|
ipcMain.handleOnce('loginWithDeviceFlow', async () => {
|
||||||
|
if (!handle) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
'No handle available. Did you call startDeviceFlow before calling this?'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
shell.openExternal(handle.verification_uri_complete).catch(reportRejection)
|
||||||
|
|
||||||
// Wait for the user to login.
|
// Wait for the user to login.
|
||||||
try {
|
try {
|
||||||
console.log('Polling for token')
|
console.log('Polling for token')
|
||||||
const tokenSet = await handle.poll()
|
const tokenSet = await handle.poll()
|
||||||
console.log('Received token set')
|
console.log('Received token set')
|
||||||
console.log(tokenSet)
|
console.log(tokenSet)
|
||||||
return tokenSet.access_token
|
return tokenSet.access_token
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error('No access token received'))
|
return Promise.reject(new Error('No access token received'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return the user code so the app can display it.
|
||||||
|
return handle.user_code
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('kittycad', (event, data) => {
|
ipcMain.handle('kittycad', (event, data) => {
|
||||||
|
@ -11,8 +11,10 @@ const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args)
|
|||||||
const openExternal = (url: any) => ipcRenderer.invoke('shell.openExternal', url)
|
const openExternal = (url: any) => ipcRenderer.invoke('shell.openExternal', url)
|
||||||
const showInFolder = (path: string) =>
|
const showInFolder = (path: string) =>
|
||||||
ipcRenderer.invoke('shell.showItemInFolder', path)
|
ipcRenderer.invoke('shell.showItemInFolder', path)
|
||||||
const login = (host: string): Promise<string> =>
|
const startDeviceFlow = (host: string): Promise<string> =>
|
||||||
ipcRenderer.invoke('login', host)
|
ipcRenderer.invoke('startDeviceFlow', host)
|
||||||
|
const loginWithDeviceFlow = (): Promise<string> =>
|
||||||
|
ipcRenderer.invoke('loginWithDeviceFlow')
|
||||||
|
|
||||||
const isMac = os.platform() === 'darwin'
|
const isMac = os.platform() === 'darwin'
|
||||||
const isWindows = os.platform() === 'win32'
|
const isWindows = os.platform() === 'win32'
|
||||||
@ -61,7 +63,8 @@ const getMachineApiIp = async (): Promise<String | null> =>
|
|||||||
ipcRenderer.invoke('find_machine_api')
|
ipcRenderer.invoke('find_machine_api')
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
login,
|
startDeviceFlow,
|
||||||
|
loginWithDeviceFlow,
|
||||||
// Passing fs directly is not recommended since it gives a lot of power
|
// Passing fs directly is not recommended since it gives a lot of power
|
||||||
// to the browser side / potential malicious code. We restrict what is
|
// to the browser side / potential malicious code. We restrict what is
|
||||||
// exported.
|
// exported.
|
||||||
|
@ -5,7 +5,7 @@ import { Themes, getSystemTheme } from '../lib/theme'
|
|||||||
import { PATHS } from 'lib/paths'
|
import { PATHS } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { CSSProperties, useCallback } from 'react'
|
import { CSSProperties, useCallback, useState } from 'react'
|
||||||
import { Logo } from 'components/Logo'
|
import { Logo } from 'components/Logo'
|
||||||
import { CustomIcon } from 'components/CustomIcon'
|
import { CustomIcon } from 'components/CustomIcon'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
@ -13,12 +13,14 @@ import { APP_VERSION } from './Settings'
|
|||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
import { toSync } from 'lib/utils'
|
import { toSync } from 'lib/utils'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
const subtleBorder =
|
const subtleBorder =
|
||||||
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
|
||||||
const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:text-chalkboard-30`
|
const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:text-chalkboard-30`
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
|
const [userCode, setUserCode] = useState('')
|
||||||
const {
|
const {
|
||||||
auth: { send },
|
auth: { send },
|
||||||
settings: {
|
settings: {
|
||||||
@ -51,12 +53,24 @@ const SignIn = () => {
|
|||||||
|
|
||||||
const signInDesktop = async () => {
|
const signInDesktop = async () => {
|
||||||
// We want to invoke our command to login via device auth.
|
// We want to invoke our command to login via device auth.
|
||||||
try {
|
const userCodeToDisplay = await window.electron
|
||||||
const token: string = await window.electron.login(VITE_KC_API_BASE_URL)
|
.startDeviceFlow(VITE_KC_API_BASE_URL)
|
||||||
send({ type: 'Log in', token })
|
.catch(reportError)
|
||||||
} catch (error) {
|
if (!userCodeToDisplay) {
|
||||||
console.error('Error with login button', error)
|
console.error('No user code received while trying to log in')
|
||||||
|
toast.error('Error while trying to log in')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
setUserCode(userCodeToDisplay)
|
||||||
|
|
||||||
|
// Now that we have the user code, we can kick off the final login step.
|
||||||
|
const token = await window.electron.loginWithDeviceFlow().catch(reportError)
|
||||||
|
if (!token) {
|
||||||
|
console.error('No token received while trying to log in')
|
||||||
|
toast.error('Error while trying to log in')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
send({ type: 'Log in', token })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -105,17 +119,40 @@ const SignIn = () => {
|
|||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
{isDesktop() ? (
|
{isDesktop() ? (
|
||||||
<button
|
<div className="flex flex-col gap-2">
|
||||||
onClick={toSync(signInDesktop, reportRejection)}
|
{!userCode ? (
|
||||||
className={
|
<button
|
||||||
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
|
onClick={toSync(signInDesktop, reportRejection)}
|
||||||
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
|
className={
|
||||||
}
|
'm-0 mt-8 w-fit flex gap-4 items-center px-3 py-1 ' +
|
||||||
data-testid="sign-in-button"
|
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
|
||||||
>
|
}
|
||||||
Sign in to get started
|
data-testid="sign-in-button"
|
||||||
<CustomIcon name="arrowRight" className="w-6 h-6" />
|
>
|
||||||
</button>
|
Sign in to get started
|
||||||
|
<CustomIcon name="arrowRight" className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-xs">
|
||||||
|
You should see the following code in your browser
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-bold inline-flex gap-1">
|
||||||
|
{userCode.split('').map((char, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={
|
||||||
|
'text-xl font-bold p-1 ' +
|
||||||
|
(char === '-' ? '' : 'border-2 border-solid')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
onClick={openExternalBrowserIfDesktop(signInUrl)}
|
onClick={openExternalBrowserIfDesktop(signInUrl)}
|
||||||
|
Reference in New Issue
Block a user