Files
upstream-rocketchat/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

119 lines
5.5 KiB
TypeScript

import { useConnectionStatus, useSession, useUser, useSetting } from '@rocket.chat/ui-contexts';
import RegistrationRoute from '@rocket.chat/web-ui-registration';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import LoggedInArea from './LoggedInArea';
import LoginPage from './LoginPage';
import UsernameCheck from './UsernameCheck';
import { useStoredItem } from '../../../hooks/useStoredItem';
import { STORAGE_KEYS } from '../../../lib/sdk/storage';
import HomeSkeleton from '../../home/HomeSkeleton';
/*
* Anonymous and guest are similar in some way
*
* Anonymous is an old feature that allows the user to navigate as an anonymus user
* by default the user dont need to do anything its hadled by the system but by behind the scenes a new * user is registered
*
* Guest is only for certain locations, it shows a form asking if the user wants to stay as guest and if so
* renders the page, without creating an user (not even an anonymous user)
*/
export type AuthenticationCheckProps = { children: ReactNode; guest?: boolean };
/**
* The connection states that mean a server was reached for and lost, as opposed to not having answered yet.
*
* `offline` is deliberately not one of them, however much it sounds like the plainest case: the DDP SDK begins
* every page load `idle`, and `sdkStatusToMeteor` reports `idle` as `offline`. Counting it would give up on the
* connection before it had been attempted, on exactly the healthy reloads this exists to protect.
*/
const hasGivenUp = (status: ReturnType<typeof useConnectionStatus>['status']): boolean => status === 'waiting' || status === 'failed';
const AuthenticationCheck = ({ children, guest }: AuthenticationCheckProps) => {
const user = useUser();
const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead');
const forceLogin = useSession('forceLogin');
const { status } = useConnectionStatus();
/**
* A resume needs a server to answer it, and waiting on one that never will is the one way this can strand
* someone: the token is only cleared when a server *rejects* it, so an unreachable server — a dropped network,
* a captive portal, a workspace that is down — clears nothing and would leave the skeleton up for good.
*
* Which states count is the whole of the care here — see `hasGivenUp`, and note that neither `connecting` nor
* `offline` is one of them. Once a state that counts has been seen it stands: a retry flapping between
* `waiting` and `connecting` must not flap the form back into a skeleton. A resume that succeeds later brings
* a user with it, which wins anyway.
*
* Seeded from the status rather than latched from `false`, so a mount that is *already* offline picks the form
* on its first render instead of showing a frame of skeleton on the way to it.
*/
const [unreachable, setUnreachable] = useState(() => hasGivenUp(status));
useEffect(() => {
if (hasGivenUp(status)) {
setUnreachable(true);
}
}, [status]);
/**
* A resume is something that happens *before* the first user of a mount, never after one. Once a user has been
* seen, a user that goes away again is a session that ended — a logout, an account deleted from under itself,
* a token the server revoked — and the form, not a skeleton, is the answer. Latching this is what keeps the
* skeleton bounded by something the component owns, rather than by every path that drops a session
* remembering to clear the stored token on its way out: deleting your own account does not, and left the
* skeleton up for good.
*/
const hasSeenUser = useRef(false);
if (user) {
hasSeenUser.current = true;
}
/**
* A window that opens with a session already stored — a call popout, or any plain reload — has no user until
* the login is resumed from that token. Treating "no user yet" as "not logged in" showed a login form for the
* few hundred milliseconds it took, to someone who never asked for one.
*
* The stored token is the whole of the test, and deliberately so. It is written before the window loads and
* removed on an explicit logout or a rejected resume, so it covers the resume from end to end. Asking
* `isLoggingIn` as well looked like it covered the same ground more directly, but it is true of *any* login in
* flight, including one someone is making at the form right now: that unmounted the form mid-attempt, so a
* rejected password came back to a blank form with nothing marked invalid, and iframe login — which runs from
* inside `LoginPage` — could never get as far as showing its own form at all.
*
* Subscribed to rather than read per render, because the writes that matter here are same-tab ones and those
* announce themselves to nobody: `clearStoredCredentials()` ends by nulling a connection userId that is
* *already* null on a resume that never got a user, so neither the Tracker dep nor the userId store fires and
* there is no next render to fall through on. See `useStoredItem`.
*/
const loginToken = useStoredItem(STORAGE_KEYS.LOGIN_TOKEN);
const isResumingSession = !user && !hasSeenUser.current && !forceLogin && !unreachable && !!loginToken;
if (isResumingSession) {
return <HomeSkeleton />;
}
if (user) {
return (
<LoggedInArea>
<UsernameCheck>{children}</UsernameCheck>
</LoggedInArea>
);
}
if (!forceLogin && guest) {
return <RegistrationRoute defaultRoute='guest'>{children}</RegistrationRoute>;
}
if (!forceLogin && allowAnonymousRead) {
return <UsernameCheck>{children}</UsernameCheck>;
}
return <LoginPage />;
};
export default AuthenticationCheck;