mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
fix-github
...
add-tos-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2ca4662d8 |
@@ -8,6 +8,7 @@ import {
|
||||
Conversation,
|
||||
ResultSet,
|
||||
GetTrajectoryResponse,
|
||||
AcceptTosResponse,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
@@ -277,6 +278,15 @@ class OpenHands {
|
||||
appMode === "saas" ? "/api/logout" : "/api/unset-settings-tokens";
|
||||
await openHands.post(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the Terms of Service
|
||||
* @returns Promise<boolean> - True if successful
|
||||
*/
|
||||
static async acceptTos(): Promise<boolean> {
|
||||
const response = await openHands.post<AcceptTosResponse>("/api/accept_tos");
|
||||
return response.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenHands;
|
||||
|
||||
@@ -83,3 +83,8 @@ export interface ResultSet<T> {
|
||||
results: T[];
|
||||
next_page_id: string | null;
|
||||
}
|
||||
|
||||
export interface AcceptTosResponse {
|
||||
message: string;
|
||||
accepted_tos: string;
|
||||
}
|
||||
|
||||
58
frontend/src/components/features/tos/tos-redirect.tsx
Normal file
58
frontend/src/components/features/tos/tos-redirect.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router";
|
||||
import { checkTosAcceptance } from "#/utils/check-tos-acceptance";
|
||||
|
||||
interface TOSRedirectProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that checks if the user has accepted the TOS and redirects to the TOS page if not
|
||||
*/
|
||||
export function TOSRedirect({ children }: TOSRedirectProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasAcceptedTos, setHasAcceptedTos] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkTos = async () => {
|
||||
try {
|
||||
// Skip the check if we're already on the TOS page or login page
|
||||
if (location.pathname === "/accept-tos") {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasAccepted = await checkTosAcceptance();
|
||||
setHasAcceptedTos(hasAccepted);
|
||||
|
||||
if (!hasAccepted) {
|
||||
navigate("/accept-tos");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check TOS acceptance:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkTos();
|
||||
}, [navigate, location.pathname]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If we're on the TOS page or the user has accepted the TOS, render the children
|
||||
if (location.pathname === "/accept-tos" || hasAcceptedTos) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// This should not happen as we redirect to the TOS page if the user hasn't accepted it
|
||||
return null;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { TOSCheckbox } from "./tos-checkbox";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
@@ -15,7 +14,6 @@ interface AuthModalProps {
|
||||
|
||||
export function AuthModal({ githubAuthUrl }: AuthModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
@@ -34,10 +32,19 @@ export function AuthModal({ githubAuthUrl }: AuthModalProps) {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
|
||||
<div className="text-sm text-muted-foreground mb-2 text-center">
|
||||
By signing in, you agree to our{" "}
|
||||
<a
|
||||
href="https://www.all-hands.dev/tos"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2 text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
{t(I18nKey.TOS$TERMS)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<BrandButton
|
||||
isDisabled={!isTosAccepted}
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleGitHubAuth}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
export default [
|
||||
layout("routes/root-layout.tsx", [
|
||||
index("routes/home.tsx"),
|
||||
route("accept-tos", "routes/accept-tos.tsx"),
|
||||
route("settings", "routes/settings.tsx", [
|
||||
index("routes/account-settings.tsx"),
|
||||
route("billing", "routes/billing.tsx"),
|
||||
|
||||
85
frontend/src/routes/accept-tos.tsx
Normal file
85
frontend/src/routes/accept-tos.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useNavigate } from "react-router";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export default function AcceptTOS() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleAcceptTOS = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const success = await OpenHands.acceptTos();
|
||||
if (success) {
|
||||
toast.success("Terms of Service accepted");
|
||||
navigate("/");
|
||||
} else {
|
||||
toast.error("Failed to accept Terms of Service");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to accept TOS:", error);
|
||||
toast.error("Failed to accept Terms of Service");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-background p-4">
|
||||
<div className="w-full max-w-md p-6 bg-card rounded-lg shadow-lg border border-tertiary">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<AllHandsLogo width={68} height={46} />
|
||||
|
||||
<h1 className="text-2xl font-bold text-center">
|
||||
Terms of Service
|
||||
</h1>
|
||||
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
<p className="mb-4">
|
||||
Please read and accept our Terms of Service to continue using OpenHands.
|
||||
</p>
|
||||
<div className="border border-tertiary rounded-md p-4 max-h-60 overflow-y-auto mb-4">
|
||||
<p className="mb-2">
|
||||
By using OpenHands, you agree to the following terms:
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-2">
|
||||
<li>You will use the service responsibly and ethically</li>
|
||||
<li>You will not use the service for illegal activities</li>
|
||||
<li>You will not attempt to circumvent security measures</li>
|
||||
<li>You understand that your usage may be monitored</li>
|
||||
<li>You acknowledge that the service is provided as-is</li>
|
||||
</ul>
|
||||
<p className="mt-4">
|
||||
For the complete terms, please visit{" "}
|
||||
<a
|
||||
href="https://www.all-hands.dev/tos"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2 text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
{t(I18nKey.TOS$TERMS)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleAcceptTOS}
|
||||
className="w-full"
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Processing..." : `${t(I18nKey.TOS$ACCEPT)} ${t(I18nKey.TOS$TERMS)}`}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
|
||||
import { useBalance } from "#/hooks/query/use-balance";
|
||||
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
|
||||
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
||||
import { TOSRedirect } from "#/components/features/tos/tos-redirect";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
@@ -120,14 +121,29 @@ export default function MainApp() {
|
||||
data-testid="root-layout"
|
||||
className="bg-base p-3 h-screen md:min-w-[1024px] overflow-x-hidden flex flex-col md:flex-row gap-3"
|
||||
>
|
||||
<Sidebar />
|
||||
{userIsAuthed ? (
|
||||
<TOSRedirect>
|
||||
<Sidebar />
|
||||
|
||||
<div
|
||||
id="root-outlet"
|
||||
className="h-[calc(100%-50px)] md:h-full w-full relative"
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
<div
|
||||
id="root-outlet"
|
||||
className="h-[calc(100%-50px)] md:h-full w-full relative"
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</TOSRedirect>
|
||||
) : (
|
||||
<>
|
||||
<Sidebar />
|
||||
|
||||
<div
|
||||
id="root-outlet"
|
||||
className="h-[calc(100%-50px)] md:h-full w-full relative"
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{renderAuthModal && <AuthModal githubAuthUrl={gitHubAuthUrl} />}
|
||||
{config.data?.APP_MODE === "oss" && consentFormIsOpen && (
|
||||
|
||||
@@ -20,6 +20,7 @@ export type Settings = {
|
||||
USER_CONSENTS_TO_ANALYTICS: boolean | null;
|
||||
PROVIDER_TOKENS: Record<Provider, string>;
|
||||
IS_NEW_USER?: boolean;
|
||||
ACCEPTED_TOS?: string | null;
|
||||
};
|
||||
|
||||
export type ApiSettings = {
|
||||
@@ -37,6 +38,7 @@ export type ApiSettings = {
|
||||
user_consents_to_analytics: boolean | null;
|
||||
provider_tokens: Record<Provider, string>;
|
||||
provider_tokens_set: Record<Provider, boolean>;
|
||||
accepted_tos: string | null;
|
||||
};
|
||||
|
||||
export type PostSettings = Settings & {
|
||||
|
||||
15
frontend/src/utils/check-tos-acceptance.ts
Normal file
15
frontend/src/utils/check-tos-acceptance.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
/**
|
||||
* Checks if the user has accepted the Terms of Service
|
||||
* @returns Promise<boolean> - True if the user has accepted the TOS, false otherwise
|
||||
*/
|
||||
export async function checkTosAcceptance(): Promise<boolean> {
|
||||
try {
|
||||
const settings = await OpenHands.getSettings();
|
||||
return !!settings.accepted_tos;
|
||||
} catch (error) {
|
||||
console.error("Failed to check TOS acceptance:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
@@ -35,6 +36,7 @@ class Settings(BaseModel):
|
||||
user_consents_to_analytics: bool | None = None
|
||||
sandbox_base_container_image: str | None = None
|
||||
sandbox_runtime_container_image: str | None = None
|
||||
accepted_tos: datetime | None = None
|
||||
|
||||
model_config = {
|
||||
'validate_assignment': True,
|
||||
|
||||
Reference in New Issue
Block a user