Fix "set username" screen (#6824)

* Fix cloud-v2/#432

* Delay setting backend to local backend; don't list directory if user is not enabled

* Add a way to debug specific dashboard paths

* Fix bug

* Check resources and status immediately
This commit is contained in:
somebody1234 2023-05-26 16:26:45 +10:00 committed by GitHub
parent 6693bdb5cd
commit 89d5b11e04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 27 deletions

View File

@ -140,7 +140,6 @@ const AuthContext = react.createContext<AuthContextType>({} as AuthContextType)
/** Props for an {@link AuthProvider}. */
export interface AuthProviderProps {
authService: authServiceModule.AuthService
platform: platformModule.Platform
/** Callback to execute once the user has authenticated successfully. */
onAuthenticated: () => void
children: react.ReactNode
@ -148,13 +147,12 @@ export interface AuthProviderProps {
/** A React provider for the Cognito API. */
export function AuthProvider(props: AuthProviderProps) {
const { authService, platform, children } = props
const { authService, onAuthenticated, children } = props
const { cognito } = authService
const { session } = sessionProvider.useSession()
const { setBackend } = backendProvider.useSetBackend()
const logger = loggerProvider.useLogger()
const navigate = router.useNavigate()
const onAuthenticated = react.useCallback(props.onAuthenticated, [])
const [initialized, setInitialized] = react.useState(false)
const [userSession, setUserSession] = react.useState<UserSession | null>(null)
@ -174,7 +172,9 @@ export function AuthProvider(props: AuthProviderProps) {
headers.append('Authorization', `Bearer ${accessToken}`)
const client = new http.Client(headers)
const backend = new remoteBackend.RemoteBackend(client, logger)
if (platform === platformModule.Platform.cloud) {
// The backend MUST be the remote backend before login is finished.
// This is because the "set username" flow requires the remote backend.
if (!initialized || userSession == null) {
setBackend(backend)
}
const organization = await backend.usersMe().catch(() => null)
@ -326,6 +326,7 @@ export function AuthProvider(props: AuthProviderProps) {
}
const signOut = async () => {
setInitialized(false)
await cognito.signOut()
toast.success(MESSAGES.signOutSuccess)
return true
@ -387,6 +388,16 @@ export function useAuth() {
return react.useContext(AuthContext)
}
// ===============================
// === shouldPreventNavigation ===
// ===============================
/** True if navigation should be prevented, for debugging purposes. */
function getShouldPreventNavigation() {
const location = router.useLocation()
return new URLSearchParams(location.search).get('prevent-navigation') === 'true'
}
// =======================
// === ProtectedLayout ===
// =======================
@ -394,10 +405,11 @@ export function useAuth() {
/** A React Router layout route containing routes only accessible by users that are logged in. */
export function ProtectedLayout() {
const { session } = useAuth()
const shouldPreventNavigation = getShouldPreventNavigation()
if (!session) {
if (!shouldPreventNavigation && !session) {
return <router.Navigate to={app.LOGIN_PATH} />
} else if (session.type === UserSessionType.partial) {
} else if (!shouldPreventNavigation && session?.type === UserSessionType.partial) {
return <router.Navigate to={app.SET_USERNAME_PATH} />
} else {
return <router.Outlet context={session} />
@ -412,8 +424,9 @@ export function ProtectedLayout() {
* in the process of registering. */
export function SemiProtectedLayout() {
const { session } = useAuth()
const shouldPreventNavigation = getShouldPreventNavigation()
if (session?.type === UserSessionType.full) {
if (!shouldPreventNavigation && session?.type === UserSessionType.full) {
return <router.Navigate to={app.DASHBOARD_PATH} />
} else {
return <router.Outlet context={session} />
@ -428,10 +441,11 @@ export function SemiProtectedLayout() {
* not logged in. */
export function GuestLayout() {
const { session } = useAuth()
const shouldPreventNavigation = getShouldPreventNavigation()
if (session?.type === UserSessionType.partial) {
if (!shouldPreventNavigation && session?.type === UserSessionType.partial) {
return <router.Navigate to={app.SET_USERNAME_PATH} />
} else if (session?.type === UserSessionType.full) {
} else if (!shouldPreventNavigation && session?.type === UserSessionType.full) {
return <router.Navigate to={app.DASHBOARD_PATH} />
} else {
return <router.Outlet />

View File

@ -39,7 +39,6 @@ import * as router from 'react-router-dom'
import * as toast from 'react-hot-toast'
import * as authService from '../authentication/service'
import * as localBackend from '../dashboard/localBackend'
import * as platformModule from '../platform'
import * as authProvider from '../authentication/providers/auth'
@ -122,8 +121,14 @@ function App(props: AppProps) {
* because the {@link AppRouter} relies on React hooks, which can't be used in the same React
* component as the component that defines the provider. */
function AppRouter(props: AppProps) {
const { logger, platform, showDashboard, onAuthenticated } = props
const { logger, showDashboard, onAuthenticated } = props
const navigate = router.useNavigate()
// FIXME[sb]: After platform detection for Electron is merged in, `IS_DEV_MODE` should be
// set to true on `ide watch`.
if (IS_DEV_MODE) {
// @ts-expect-error This is used exclusively for debugging.
window.navigate = navigate
}
const mainPageUrl = new URL(window.location.href)
const memoizedAuthService = react.useMemo(() => {
const authConfig = { navigate, ...props }
@ -164,20 +169,12 @@ function AppRouter(props: AppProps) {
userSession={userSession}
registerAuthEventListener={registerAuthEventListener}
>
<backendProvider.BackendProvider
initialBackend={
platform === platformModule.Platform.desktop
? new localBackend.LocalBackend()
: // This is UNSAFE. However, the backend will be set by the
// authentication flow.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
null!
}
>
{/* This is safe, because the backend is always set by the authentication flow. */}
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
<backendProvider.BackendProvider initialBackend={null!}>
<authProvider.AuthProvider
authService={memoizedAuthService}
onAuthenticated={onAuthenticated}
platform={platform}
>
<modalProvider.ModalProvider>{routes}</modalProvider.ModalProvider>
</authProvider.AuthProvider>

View File

@ -278,9 +278,17 @@ function Dashboard(props: DashboardProps) {
backendModule.Asset<backendModule.AssetType.file>[]
>([])
const canListDirectory =
backend.platform !== platformModule.Platform.cloud || organization.isEnabled
const directory = directoryStack[directoryStack.length - 1]
const parentDirectory = directoryStack[directoryStack.length - 2]
react.useEffect(() => {
if (platform === platformModule.Platform.desktop) {
setBackend(new localBackend.LocalBackend())
}
}, [])
react.useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (
@ -398,6 +406,7 @@ function Dashboard(props: DashboardProps) {
<ProjectActionButton
project={projectAsset}
appRunner={appRunner}
doRefresh={doRefresh}
onClose={() => {
setProject(null)
}}
@ -603,10 +612,14 @@ function Dashboard(props: DashboardProps) {
hooks.useAsyncEffect(
null,
async signal => {
const assets = await backend.listDirectory({ parentId: directoryId })
if (!signal.aborted) {
if (canListDirectory) {
const assets = await backend.listDirectory({ parentId: directoryId })
if (!signal.aborted) {
setIsLoadingAssets(false)
setAssets(assets)
}
} else {
setIsLoadingAssets(false)
setAssets(assets)
}
},
[accessToken, directoryId, refresh, backend]
@ -728,7 +741,7 @@ function Dashboard(props: DashboardProps) {
query={query}
setQuery={setQuery}
/>
{backend.platform === platformModule.Platform.cloud && !organization.isEnabled ? (
{!canListDirectory ? (
<div className="grow grid place-items-center">
<div className="text-base text-center">
We will review your user details and enable the cloud experience for you

View File

@ -43,11 +43,12 @@ export interface ProjectActionButtonProps {
appRunner: AppRunner | null
onClose: () => void
openIde: () => void
doRefresh: () => void
}
/** An interactive button displaying the status of a project. */
function ProjectActionButton(props: ProjectActionButtonProps) {
const { project, onClose, appRunner, openIde } = props
const { project, onClose, appRunner, openIde, doRefresh } = props
const { backend } = backendProvider.useBackend()
const [state, setState] = react.useState(backendModule.ProjectState.created)
@ -101,6 +102,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
() => void checkProjectStatus(),
CHECK_STATUS_INTERVAL_MS
)
void checkProjectStatus()
return () => {
clearInterval(handle)
}
@ -132,6 +134,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
() => void checkProjectResources(),
CHECK_RESOURCES_INTERVAL_MS
)
void checkProjectResources()
return () => {
clearInterval(handle)
}
@ -159,10 +162,12 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
switch (backend.platform) {
case platform.Platform.cloud:
await backend.openProject(project.id)
doRefresh()
setIsCheckingStatus(true)
break
case platform.Platform.desktop:
await backend.openProject(project.id)
doRefresh()
setState(backendModule.ProjectState.opened)
setSpinnerState(SpinnerState.done)
break