go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/core/pages/login_page.tsx (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { Link } from '@mui/material'; 16 import { useEffect } from 'react'; 17 import { useNavigate } from 'react-router-dom'; 18 19 import { ANONYMOUS_IDENTITY } from '@/common/api/auth_state'; 20 import { useAuthState } from '@/common/components/auth_state_provider'; 21 import { RecoverableErrorBoundary } from '@/common/components/error_handling'; 22 import { getLoginUrl } from '@/common/tools/url_utils'; 23 import { useSyncedSearchParams } from '@/generic_libs/hooks/synced_search_params'; 24 25 /** 26 * Prompts the user to login. 27 * Once logged in, redirects to 28 * - URL specified in 'redirect' search param, or 29 * - root. 30 * in that order. 31 */ 32 export function LoginPage() { 33 const navigate = useNavigate(); 34 const authState = useAuthState(); 35 const [searchParam] = useSyncedSearchParams(); 36 const redirectTo = searchParam.get('redirect') || '/'; 37 38 const isLoggedIn = authState.identity !== ANONYMOUS_IDENTITY; 39 40 // Perform redirection directly if the user is already logged in. This might 41 // happen if the user logged in via another browser tab and the auth state is 42 // refreshed. 43 useEffect(() => { 44 if (!isLoggedIn) { 45 return; 46 } 47 navigate(redirectTo); 48 }, [isLoggedIn, navigate, redirectTo]); 49 50 return ( 51 <div css={{ margin: '8px 16px' }}> 52 You must{' '} 53 <Link 54 href={getLoginUrl(redirectTo)} 55 css={{ textDecoration: 'underline', cursor: 'pointer' }} 56 > 57 login 58 </Link>{' '} 59 to see anything useful. 60 </div> 61 ); 62 } 63 64 export const element = ( 65 // We cannot use `<RecoverableErrorBoundary />` in `errorElement` because it 66 // (react-router) doesn't support error recovery. 67 // 68 // We handle the error at child level rather than at the parent level because 69 // we want the error state to be reset when the user navigates to a sibling 70 // view, which does not happen if the error is handled by the parent (without 71 // additional logic). 72 // The downside of this model is that we do not have a central place for error 73 // handling, which is somewhat mitigated by applying the same error boundary 74 // on all child routes. 75 // The upside is that the error is naturally reset on route changes. 76 // 77 // A unique `key` is needed to ensure the boundary is not reused when the user 78 // navigates to a sibling view. The error will be naturally discarded as the 79 // route is unmounted. 80 <RecoverableErrorBoundary key="login"> 81 <LoginPage /> 82 </RecoverableErrorBoundary> 83 );