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
119 lines
5.5 KiB
TypeScript
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;
|