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  );