Xstate Auth migration (#250)
* auth migrate progress, web only * wrap home in state provider * use consistent logged spelling * use createActorContext * typo * fix wraping problem
This commit is contained in:
@ -19,6 +19,7 @@
|
|||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||||
"@uiw/react-codemirror": "^4.15.1",
|
"@uiw/react-codemirror": "^4.15.1",
|
||||||
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
@ -42,6 +43,7 @@
|
|||||||
"wasm-pack": "^0.12.1",
|
"wasm-pack": "^0.12.1",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.13.0",
|
||||||
|
"xstate": "^4.38.2",
|
||||||
"zustand": "^4.1.4"
|
"zustand": "^4.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'
|
|||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
import { describe, test, vi } from 'vitest'
|
import { describe, test, vi } from 'vitest'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
||||||
|
|
||||||
let listener: ((rect: any) => void) | undefined = undefined
|
let listener: ((rect: any) => void) | undefined = undefined
|
||||||
;(global as any).ResizeObserver = class ResizeObserver {
|
;(global as any).ResizeObserver = class ResizeObserver {
|
||||||
@ -27,9 +28,9 @@ describe('App tests', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<TestWrap>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</TestWrap>
|
||||||
)
|
)
|
||||||
const linkElement = screen.getByText(/Variables/i)
|
const linkElement = screen.getByText(/Variables/i)
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
@ -37,3 +38,12 @@ describe('App tests', () => {
|
|||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
|
// wrap in router and xState context
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -48,6 +48,7 @@ import { writeTextFile } from '@tauri-apps/api/fs'
|
|||||||
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
||||||
import { IndexLoaderData } from './Router'
|
import { IndexLoaderData } from './Router'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { useAuthMachine } from './hooks/useAuthMachine'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
@ -81,7 +82,6 @@ export function App() {
|
|||||||
isMouseDownInStream,
|
isMouseDownInStream,
|
||||||
cmdId,
|
cmdId,
|
||||||
setCmdId,
|
setCmdId,
|
||||||
token,
|
|
||||||
formatCode,
|
formatCode,
|
||||||
debugPanel,
|
debugPanel,
|
||||||
theme,
|
theme,
|
||||||
@ -121,7 +121,6 @@ export function App() {
|
|||||||
isMouseDownInStream: s.isMouseDownInStream,
|
isMouseDownInStream: s.isMouseDownInStream,
|
||||||
cmdId: s.cmdId,
|
cmdId: s.cmdId,
|
||||||
setCmdId: s.setCmdId,
|
setCmdId: s.setCmdId,
|
||||||
token: s.token,
|
|
||||||
formatCode: s.formatCode,
|
formatCode: s.formatCode,
|
||||||
debugPanel: s.debugPanel,
|
debugPanel: s.debugPanel,
|
||||||
addKCLError: s.addKCLError,
|
addKCLError: s.addKCLError,
|
||||||
@ -134,6 +133,7 @@ export function App() {
|
|||||||
setStreamDimensions: s.setStreamDimensions,
|
setStreamDimensions: s.setStreamDimensions,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
|
const [token] = useAuthMachine((s) => s?.context?.token)
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
|
|
||||||
|
33
src/Auth.tsx
33
src/Auth.tsx
@ -1,38 +1,11 @@
|
|||||||
import useSWR from 'swr'
|
|
||||||
import fetcher from './lib/fetcher'
|
|
||||||
import withBaseUrl from './lib/withBaseURL'
|
|
||||||
import { User, useStore } from './useStore'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { isTauri } from './lib/isTauri'
|
|
||||||
import Loading from './components/Loading'
|
import Loading from './components/Loading'
|
||||||
import { paths } from './Router'
|
import { useAuthMachine } from './hooks/useAuthMachine'
|
||||||
|
|
||||||
// Wrapper around protected routes, used in src/Router.tsx
|
// Wrapper around protected routes, used in src/Router.tsx
|
||||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||||
const { data: user, isLoading } = useSWR<
|
const [isLoggedIn] = useAuthMachine((s) => s.matches('checkIfLoggedIn'))
|
||||||
User | Partial<{ error_code: string }>
|
|
||||||
>(withBaseUrl('/user'), fetcher)
|
|
||||||
const { token, setUser } = useStore((s) => ({
|
|
||||||
token: s.token,
|
|
||||||
setUser: s.setUser,
|
|
||||||
}))
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
useEffect(() => {
|
return isLoggedIn ? (
|
||||||
if (user && 'id' in user) setUser(user)
|
|
||||||
}, [user, setUser])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
(isTauri() && !token) ||
|
|
||||||
(!isTauri() && !isLoading && !(user && 'id' in user))
|
|
||||||
) {
|
|
||||||
navigate(paths.SIGN_IN)
|
|
||||||
}
|
|
||||||
}, [user, token, navigate, isLoading])
|
|
||||||
|
|
||||||
return isLoading ? (
|
|
||||||
<Loading>Loading KittyCAD Modeling App...</Loading>
|
<Loading>Loading KittyCAD Modeling App...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
} from './lib/tauriFS'
|
} from './lib/tauriFS'
|
||||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||||
|
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
||||||
|
|
||||||
const prependRoutes =
|
const prependRoutes =
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
@ -58,7 +59,22 @@ export type HomeLoaderData = {
|
|||||||
projects: ProjectWithEntryPointMetadata[]
|
projects: ProjectWithEntryPointMetadata[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
type CreateBrowserRouterArg = Parameters<typeof createBrowserRouter>[0]
|
||||||
|
|
||||||
|
const addGlobalContextToElements = (
|
||||||
|
routes: CreateBrowserRouterArg
|
||||||
|
): CreateBrowserRouterArg =>
|
||||||
|
routes.map((route) =>
|
||||||
|
'element' in route
|
||||||
|
? {
|
||||||
|
...route,
|
||||||
|
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>,
|
||||||
|
}
|
||||||
|
: route
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = createBrowserRouter(
|
||||||
|
addGlobalContextToElements([
|
||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
loader: () =>
|
loader: () =>
|
||||||
@ -95,7 +111,9 @@ const router = createBrowserRouter([
|
|||||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||||
|
|
||||||
if (shouldRedirectToOnboarding) {
|
if (shouldRedirectToOnboarding) {
|
||||||
return redirect(makeUrlPathRelative(paths.ONBOARDING.INDEX) + status)
|
return redirect(
|
||||||
|
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +194,7 @@ const router = createBrowserRouter([
|
|||||||
element: <SignIn />,
|
element: <SignIn />,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All routes in the app, used in src/index.tsx
|
* All routes in the app, used in src/index.tsx
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Toolbar } from '../Toolbar'
|
import { Toolbar } from '../Toolbar'
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
|
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
@ -18,9 +18,7 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const { user } = useStore((s) => ({
|
const [user] = useAuthMachine((s) => s?.context?.user)
|
||||||
user: s.user,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { User } from '../useStore'
|
|
||||||
import UserSidebarMenu from './UserSidebarMenu'
|
import UserSidebarMenu from './UserSidebarMenu'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { GlobalStateProvider } from '../hooks/useAuthMachine'
|
||||||
|
|
||||||
|
type User = Models['User_type']
|
||||||
|
|
||||||
describe('UserSidebarMenu tests', () => {
|
describe('UserSidebarMenu tests', () => {
|
||||||
test("Renders user's name and email if available", () => {
|
test("Renders user's name and email if available", () => {
|
||||||
@ -12,12 +15,18 @@ describe('UserSidebarMenu tests', () => {
|
|||||||
image: 'https://placekitten.com/200/200',
|
image: 'https://placekitten.com/200/200',
|
||||||
created_at: 'yesteryear',
|
created_at: 'yesteryear',
|
||||||
updated_at: 'today',
|
updated_at: 'today',
|
||||||
|
company: 'Test Company',
|
||||||
|
discord: 'Test User#1234',
|
||||||
|
github: 'testuser',
|
||||||
|
phone: '555-555-5555',
|
||||||
|
first_name: 'Test',
|
||||||
|
last_name: 'User',
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<TestWrap>
|
||||||
<UserSidebarMenu user={userWellFormed} />
|
<UserSidebarMenu user={userWellFormed} />
|
||||||
</BrowserRouter>
|
</TestWrap>
|
||||||
)
|
)
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||||
@ -35,12 +44,19 @@ describe('UserSidebarMenu tests', () => {
|
|||||||
image: 'https://placekitten.com/200/200',
|
image: 'https://placekitten.com/200/200',
|
||||||
created_at: 'yesteryear',
|
created_at: 'yesteryear',
|
||||||
updated_at: 'today',
|
updated_at: 'today',
|
||||||
|
company: 'Test Company',
|
||||||
|
discord: 'Test User#1234',
|
||||||
|
github: 'testuser',
|
||||||
|
phone: '555-555-5555',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
name: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<TestWrap>
|
||||||
<UserSidebarMenu user={userNoName} />
|
<UserSidebarMenu user={userNoName} />
|
||||||
</BrowserRouter>
|
</TestWrap>
|
||||||
)
|
)
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
fireEvent.click(screen.getByTestId('user-sidebar-toggle'))
|
||||||
@ -55,14 +71,30 @@ describe('UserSidebarMenu tests', () => {
|
|||||||
email: 'kittycad.sidebar.test@example.com',
|
email: 'kittycad.sidebar.test@example.com',
|
||||||
created_at: 'yesteryear',
|
created_at: 'yesteryear',
|
||||||
updated_at: 'today',
|
updated_at: 'today',
|
||||||
|
company: 'Test Company',
|
||||||
|
discord: 'Test User#1234',
|
||||||
|
github: 'testuser',
|
||||||
|
phone: '555-555-5555',
|
||||||
|
first_name: 'Test',
|
||||||
|
last_name: 'User',
|
||||||
|
image: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<TestWrap>
|
||||||
<UserSidebarMenu user={userNoAvatar} />
|
<UserSidebarMenu user={userNoAvatar} />
|
||||||
</BrowserRouter>
|
</TestWrap>
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu')
|
expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
|
// wrap in router and xState context
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<GlobalStateProvider>{children}</GlobalStateProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { User, useStore } from '../useStore'
|
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faBars, faGear, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||||
@ -7,14 +6,16 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
import makeUrlPathRelative from '../lib/makeUrlPathRelative'
|
||||||
|
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
|
||||||
|
type User = Models['User_type']
|
||||||
|
|
||||||
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||||
const displayedName = getDisplayName(user)
|
const displayedName = getDisplayName(user)
|
||||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { setToken } = useStore((s) => ({
|
const [_, send] = useAuthMachine()
|
||||||
setToken: s.setToken,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Fallback logic for displaying user's "name":
|
// Fallback logic for displaying user's "name":
|
||||||
// 1. user.name
|
// 1. user.name
|
||||||
@ -119,10 +120,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => {
|
onClick={() => send('logout')}
|
||||||
setToken('')
|
|
||||||
navigate(paths.SIGN_IN)
|
|
||||||
}}
|
|
||||||
icon={{
|
icon={{
|
||||||
icon: faSignOutAlt,
|
icon: faSignOutAlt,
|
||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
|
54
src/hooks/useAuthMachine.tsx
Normal file
54
src/hooks/useAuthMachine.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { createActorContext } from '@xstate/react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { paths } from '../Router'
|
||||||
|
import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine'
|
||||||
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
|
|
||||||
|
export const AuthMachineContext = createActorContext(authMachine)
|
||||||
|
|
||||||
|
export const GlobalStateProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
return (
|
||||||
|
<AuthMachineContext.Provider
|
||||||
|
machine={() =>
|
||||||
|
authMachine.withConfig({
|
||||||
|
actions: {
|
||||||
|
goToSignInPage: () => {
|
||||||
|
navigate(paths.SIGN_IN)
|
||||||
|
logout()
|
||||||
|
},
|
||||||
|
goToIndexPage: () => navigate(paths.INDEX),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthMachineContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuthMachine<T>(
|
||||||
|
selector: (
|
||||||
|
state: Parameters<Parameters<typeof AuthMachineContext.useSelector>[0]>[0]
|
||||||
|
) => T = () => null as T
|
||||||
|
): [T, ReturnType<typeof AuthMachineContext.useActor>[1]] {
|
||||||
|
// useActor api normally `[state, send] = useActor`
|
||||||
|
// we're only interested in send because of the selector
|
||||||
|
const send = AuthMachineContext.useActor()[1]
|
||||||
|
|
||||||
|
const selection = AuthMachineContext.useSelector(selector)
|
||||||
|
return [selection, send]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
const url = withBaseUrl('/logout')
|
||||||
|
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
}
|
@ -363,7 +363,6 @@ export class EngineCommandManager {
|
|||||||
this.onHoverCallback = callback
|
this.onHoverCallback = callback
|
||||||
}
|
}
|
||||||
onClick(callback: (selection?: SelectionsArgs) => void) {
|
onClick(callback: (selection?: SelectionsArgs) => void) {
|
||||||
// TODO talk to the gang about this
|
|
||||||
// It's when the user clicks on a part in the 3d scene, and so the engine should tell the
|
// It's when the user clicks on a part in the 3d scene, and so the engine should tell the
|
||||||
// frontend about that (with it's id) so that the FE can put the user's cursor on the right
|
// frontend about that (with it's id) so that the FE can put the user's cursor on the right
|
||||||
// line of code
|
// line of code
|
||||||
|
@ -136,11 +136,6 @@ export const lineTo: SketchLineHelper = {
|
|||||||
sourceRange,
|
sourceRange,
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
// engineCommandManager.sendModellingCommand({
|
|
||||||
// id,
|
|
||||||
// params: [lineData, previousSketch],
|
|
||||||
// range: sourceRange,
|
|
||||||
// })
|
|
||||||
const currentPath: Path = {
|
const currentPath: Path = {
|
||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
to,
|
to,
|
||||||
@ -673,11 +668,6 @@ export const angledLine: SketchLineHelper = {
|
|||||||
sourceRange,
|
sourceRange,
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
// engineCommandManager.sendModellingCommand({
|
|
||||||
// id,
|
|
||||||
// params: [lineData, previousSketch],
|
|
||||||
// range: sourceRange,
|
|
||||||
// })
|
|
||||||
const currentPath: Path = {
|
const currentPath: Path = {
|
||||||
type: 'toPoint',
|
type: 'toPoint',
|
||||||
to,
|
to,
|
||||||
|
102
src/lib/authMachine.ts
Normal file
102
src/lib/authMachine.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { createMachine, assign } from 'xstate'
|
||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
|
|
||||||
|
export interface UserContext {
|
||||||
|
user?: Models['User_type']
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Events =
|
||||||
|
| {
|
||||||
|
type: 'logout'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'tryLogin'
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||||
|
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||||
|
|
||||||
|
export const authMachine = createMachine<UserContext, Events>(
|
||||||
|
{
|
||||||
|
id: 'Auth',
|
||||||
|
initial: 'checkIfLoggedIn',
|
||||||
|
states: {
|
||||||
|
checkIfLoggedIn: {
|
||||||
|
id: 'check-if-logged-in',
|
||||||
|
invoke: {
|
||||||
|
src: 'getUser',
|
||||||
|
id: 'check-logged-in',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'loggedIn',
|
||||||
|
actions: assign({
|
||||||
|
user: (context, event) => event.data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'loggedOut',
|
||||||
|
actions: assign({
|
||||||
|
user: () => undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loggedIn: {
|
||||||
|
entry: ['goToIndexPage'],
|
||||||
|
on: {
|
||||||
|
logout: {
|
||||||
|
target: 'loggedOut',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loggedOut: {
|
||||||
|
entry: ['goToSignInPage'],
|
||||||
|
on: {
|
||||||
|
tryLogin: {
|
||||||
|
target: 'checkIfLoggedIn',
|
||||||
|
actions: assign({
|
||||||
|
token: (context, event) => {
|
||||||
|
const token = event.token || ''
|
||||||
|
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
||||||
|
return token
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: { events: {} as { type: 'logout' } | { type: 'tryLogin' } },
|
||||||
|
predictableActionArguments: true,
|
||||||
|
preserveActionOrder: true,
|
||||||
|
context: { token: persistedToken },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {},
|
||||||
|
services: { getUser },
|
||||||
|
guards: {},
|
||||||
|
delays: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function getUser(context: UserContext) {
|
||||||
|
const url = withBaseURL('/user')
|
||||||
|
const headers: { [key: string]: string } = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (!context.token && '__TAURI__' in window) throw 'not log in'
|
||||||
|
if (context.token) headers['Authorization'] = `Bearer ${context.token}`
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
const user = await response.json()
|
||||||
|
if ('error_code' in user) throw new Error(user.message)
|
||||||
|
return user
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import { useStore } from '../useStore'
|
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||||
|
|
||||||
export default async function fetcher<JSON = any>(
|
export default async function fetcher<JSON = any>(
|
||||||
input: RequestInfo,
|
input: RequestInfo,
|
||||||
init: RequestInit = {}
|
init: RequestInit = {}
|
||||||
): Promise<JSON> {
|
): Promise<JSON> {
|
||||||
const { token } = useStore.getState()
|
const [token] = useAuthMachine((s) => s?.context?.token)
|
||||||
const headers = { ...init.headers } as Record<string, string>
|
const headers = { ...init.headers } as Record<string, string>
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.Authorization = `Bearer ${token}`
|
headers.Authorization = `Bearer ${token}`
|
||||||
|
@ -7,13 +7,14 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
||||||
import { getSystemTheme } from '../lib/getSystemTheme'
|
import { getSystemTheme } from '../lib/getSystemTheme'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
|
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { setToken, theme } = useStore((s) => ({
|
const { theme } = useStore((s) => ({
|
||||||
setToken: s.setToken,
|
|
||||||
theme: s.theme,
|
theme: s.theme,
|
||||||
}))
|
}))
|
||||||
|
const [_, send] = useAuthMachine()
|
||||||
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
|
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
const signInTauri = async () => {
|
const signInTauri = async () => {
|
||||||
// We want to invoke our command to login via device auth.
|
// We want to invoke our command to login via device auth.
|
||||||
@ -21,8 +22,7 @@ const SignIn = () => {
|
|||||||
const token: string = await invoke('login', {
|
const token: string = await invoke('login', {
|
||||||
host: VITE_KC_API_BASE_URL,
|
host: VITE_KC_API_BASE_URL,
|
||||||
})
|
})
|
||||||
setToken(token)
|
send({ type: 'tryLogin', token })
|
||||||
navigate(paths.INDEX)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('login button', error)
|
console.error('login button', error)
|
||||||
}
|
}
|
||||||
|
@ -114,20 +114,6 @@ interface DefaultDir {
|
|||||||
|
|
||||||
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
|
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
|
||||||
|
|
||||||
// TODO: import real OpenAPI User type from schema
|
|
||||||
export interface User {
|
|
||||||
company?: string
|
|
||||||
created_at: string
|
|
||||||
email: string
|
|
||||||
first_name?: string
|
|
||||||
id: string
|
|
||||||
image?: string
|
|
||||||
last_name?: string
|
|
||||||
name?: string
|
|
||||||
phone?: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
editorView: EditorView | null
|
editorView: EditorView | null
|
||||||
setEditorView: (editorView: EditorView) => void
|
setEditorView: (editorView: EditorView) => void
|
||||||
@ -219,10 +205,6 @@ export interface StoreState {
|
|||||||
path: string
|
path: string
|
||||||
}[]
|
}[]
|
||||||
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
||||||
token: string
|
|
||||||
setToken: (token: string) => void
|
|
||||||
user?: User
|
|
||||||
setUser: (user: User | undefined) => void
|
|
||||||
debugPanel: boolean
|
debugPanel: boolean
|
||||||
setDebugPanel: (debugPanel: boolean) => void
|
setDebugPanel: (debugPanel: boolean) => void
|
||||||
}
|
}
|
||||||
@ -423,10 +405,6 @@ export const useStore = create<StoreState>()(
|
|||||||
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
||||||
homeMenuItems: [],
|
homeMenuItems: [],
|
||||||
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
||||||
token: '',
|
|
||||||
setToken: (token) => set({ token }),
|
|
||||||
user: undefined,
|
|
||||||
setUser: (user) => set({ user }),
|
|
||||||
debugPanel: false,
|
debugPanel: false,
|
||||||
setDebugPanel: (debugPanel) => set({ debugPanel }),
|
setDebugPanel: (debugPanel) => set({ debugPanel }),
|
||||||
}),
|
}),
|
||||||
@ -441,7 +419,6 @@ export const useStore = create<StoreState>()(
|
|||||||
'defaultProjectName',
|
'defaultProjectName',
|
||||||
'defaultUnitSystem',
|
'defaultUnitSystem',
|
||||||
'defaultBaseUnit',
|
'defaultBaseUnit',
|
||||||
'token',
|
|
||||||
'debugPanel',
|
'debugPanel',
|
||||||
'onboardingStatus',
|
'onboardingStatus',
|
||||||
'theme',
|
'theme',
|
||||||
|
17
yarn.lock
17
yarn.lock
@ -2506,6 +2506,14 @@
|
|||||||
loupe "^2.3.6"
|
loupe "^2.3.6"
|
||||||
pretty-format "^29.5.0"
|
pretty-format "^29.5.0"
|
||||||
|
|
||||||
|
"@xstate/react@^3.2.2":
|
||||||
|
version "3.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8"
|
||||||
|
integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==
|
||||||
|
dependencies:
|
||||||
|
use-isomorphic-layout-effect "^1.1.2"
|
||||||
|
use-sync-external-store "^1.0.0"
|
||||||
|
|
||||||
acorn-jsx@^5.3.2:
|
acorn-jsx@^5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||||
@ -5893,7 +5901,7 @@ use-composed-ref@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
|
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
|
||||||
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
|
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
|
||||||
|
|
||||||
use-isomorphic-layout-effect@^1.1.1:
|
use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||||
@ -5905,7 +5913,7 @@ use-latest@^1.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.1.1"
|
use-isomorphic-layout-effect "^1.1.1"
|
||||||
|
|
||||||
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
|
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
@ -6112,6 +6120,11 @@ ws@^8.13.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||||
|
|
||||||
|
xstate@^4.38.2:
|
||||||
|
version "4.38.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
|
||||||
|
integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==
|
||||||
|
|
||||||
yallist@^3.0.2:
|
yallist@^3.0.2:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||||
|
Reference in New Issue
Block a user