[ALL-543] feat(frontend): Setup auth route, replace loading spinner, add new route (#4448)

This commit is contained in:
sp.wack
2024-10-18 19:32:46 +04:00
committed by GitHub
parent 56fe905241
commit cf793582a7
7 changed files with 129 additions and 31 deletions

View File

@@ -210,6 +210,23 @@ class OpenHands {
return response.json();
}
/**
* Check if the user is authenticated
* @param login The user's GitHub login handle
* @returns Whether the user is authenticated
*/
static async isAuthenticated(login: string): Promise<boolean> {
const response = await fetch(`${OpenHands.BASE_URL}/authenticate`, {
method: "POST",
body: JSON.stringify({ login }),
headers: {
"Content-Type": "application/json",
},
});
return response.status === 200;
}
}
export default OpenHands;

View File

@@ -4,7 +4,6 @@ import {
json,
redirect,
useLoaderData,
useNavigation,
useRouteLoaderData,
} from "@remix-run/react";
import React from "react";
@@ -21,7 +20,6 @@ import ModalButton from "#/components/buttons/ModalButton";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import { ConnectToGitHubModal } from "#/components/modals/connect-to-github-modal";
import { ModalBackdrop } from "#/components/modals/modal-backdrop";
import { LoadingSpinner } from "#/components/modals/LoadingProject";
import store, { RootState } from "#/store";
import { removeFile, setInitialQuery } from "#/state/initial-query-slice";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
@@ -102,7 +100,6 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
function Home() {
const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
const navigation = useNavigation();
const { repositories, githubAuthUrl } = useLoaderData<typeof clientLoader>();
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
React.useState(false);
@@ -124,11 +121,6 @@ function Home() {
return (
<div className="bg-root-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto">
{navigation.state === "loading" && (
<div className="absolute top-8 right-8">
<LoadingSpinner size="small" />
</div>
)}
<HeroHeading />
<div className="flex flex-col gap-16 w-[600px] items-center">
<div className="flex flex-col gap-2 w-full">

View File

@@ -7,6 +7,7 @@ import {
json,
ClientActionFunctionArgs,
useRouteLoaderData,
redirect,
} from "@remix-run/react";
import { useDispatch, useSelector } from "react-redux";
import WebSocket from "ws";
@@ -42,6 +43,8 @@ import { base64ToBlob } from "#/utils/base64-to-blob";
import { clientLoader as rootClientLoader } from "#/routes/_oh";
import { clearJupyter } from "#/state/jupyterSlice";
import { FilesProvider } from "#/context/files";
import { clearSession } from "#/utils/clear-session";
import { userIsAuthenticated } from "#/utils/user-is-authenticated";
const isAgentStateChange = (
data: object,
@@ -51,6 +54,14 @@ const isAgentStateChange = (
"agent_state" in data.extras;
export const clientLoader = async () => {
const ghToken = localStorage.getItem("ghToken");
const isAuthed = await userIsAuthenticated(ghToken);
if (!isAuthed) {
clearSession();
return redirect("/waitlist");
}
const q = store.getState().initalQuery.initialQuery;
const repo =
store.getState().initalQuery.selectedRepository ||
@@ -59,7 +70,6 @@ export const clientLoader = async () => {
const settings = getSettings();
const token = localStorage.getItem("token");
const ghToken = localStorage.getItem("ghToken");
if (token && importedProject) {
const blob = base64ToBlob(importedProject);

View File

@@ -15,7 +15,7 @@ import CogTooth from "#/assets/cog-tooth";
import { SettingsForm } from "#/components/form/settings-form";
import AccountSettingsModal from "#/components/modals/AccountSettingsModal";
import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal";
import LoadingProjectModal from "#/components/modals/LoadingProject";
import { LoadingSpinner } from "#/components/modals/LoadingProject";
import { ModalBackdrop } from "#/components/modals/modal-backdrop";
import { UserAvatar } from "#/components/user-avatar";
import { useSocket } from "#/context/socket";
@@ -173,16 +173,22 @@ export default function MainApp() {
return (
<div className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3">
<aside className="px-1 flex flex-col gap-[15px]">
<button
type="button"
aria-label="All Hands Logo"
onClick={() => {
if (location.pathname !== "/") setStartNewProjectModalIsOpen(true);
}}
>
<AllHandsLogo width={34} height={23} />
</button>
<aside className="px-1 flex flex-col gap-1">
<div className="w-[34px] h-[34px] flex items-center justify-center">
{navigation.state === "loading" && <LoadingSpinner size="small" />}
{navigation.state !== "loading" && (
<button
type="button"
aria-label="All Hands Logo"
onClick={() => {
if (location.pathname !== "/")
setStartNewProjectModalIsOpen(true);
}}
>
<AllHandsLogo width={34} height={23} />
</button>
)}
</div>
<nav className="py-[18px] flex flex-col items-center gap-[18px]">
<UserAvatar
user={user}
@@ -222,17 +228,6 @@ export default function MainApp() {
</aside>
<div className="h-full w-full relative">
<Outlet />
{navigation.state === "loading" && location.pathname !== "/" && (
<ModalBackdrop>
<LoadingProjectModal
message={
endSessionFetcher.state === "loading"
? "Ending session, please wait..."
: undefined
}
/>
</ModalBackdrop>
)}
{(!settingsIsUpdated || settingsModalIsOpen) && (
<ModalBackdrop onClose={() => setSettingsModalIsOpen(false)}>
<div className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2">

View File

@@ -0,0 +1,39 @@
import { Link } from "@remix-run/react";
import Clipboard from "#/assets/clipboard.svg?react";
function Waitlist() {
return (
<div className="bg-neutral-800 h-full flex items-center justify-center rounded-xl">
<div className="w-[384px] flex flex-col gap-6 bg-neutral-900 rounded-xl p-6">
<Clipboard className="w-14 self-center" />
<div className="flex flex-col gap-2">
<h1 className="text-[20px] leading-6 -tracking-[0.01em] font-semibold">
You&apos;re not in the waitlist yet!
</h1>
<p className="text-neutral-400 text-xs">
Please click{" "}
<a
href="https://www.all-hands.dev/join-waitlist"
target="_blank"
rel="noreferrer noopener"
className="text-blue-500"
>
here
</a>{" "}
to join the waitlist.
</p>
</div>
<Link
to="/"
className="text-white text-sm py-[10px] bg-neutral-500 rounded text-center"
>
Go back to home
</Link>
</div>
</div>
);
}
export default Waitlist;

View File

@@ -0,0 +1,16 @@
import { retrieveGitHubUser, isGitHubErrorReponse } from "#/api/github";
import OpenHands from "#/api/open-hands";
export const userIsAuthenticated = async (ghToken: string | null) => {
if (window.__APP_MODE__ === "oss") return true;
let user: GitHubUser | GitHubErrorReponse | null = null;
if (ghToken) user = await retrieveGitHubUser(ghToken);
if (user && !isGitHubErrorReponse(user)) {
const isAuthed = await OpenHands.isAuthenticated(user.login);
return isAuthed;
}
return false;
};

View File

@@ -798,4 +798,33 @@ def github_callback(auth_code: AuthCode):
)
class User(BaseModel):
login: str # GitHub login handle
@app.post('/authenticate')
def authenticate(user: User | None = None):
waitlist = os.getenv('GITHUB_USER_LIST_FILE')
# Only check if waitlist is provided
if waitlist is not None:
try:
with open(waitlist, 'r') as f:
users = f.read().splitlines()
if user is None or user.login not in users:
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={'error': 'User not on waitlist'},
)
except FileNotFoundError:
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': 'Waitlist file not found'},
)
return JSONResponse(
status_code=status.HTTP_200_OK, content={'message': 'User authenticated'}
)
app.mount('/', StaticFiles(directory='./frontend/build', html=True), name='dist')