mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 07:18:10 -05:00
[ALL-543] feat(frontend): Setup auth route, replace loading spinner, add new route (#4448)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
39
frontend/src/routes/_oh.waitlist.tsx
Normal file
39
frontend/src/routes/_oh.waitlist.tsx
Normal 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'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;
|
||||
16
frontend/src/utils/user-is-authenticated.ts
Normal file
16
frontend/src/utils/user-is-authenticated.ts
Normal 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;
|
||||
};
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user