diff --git a/.gitignore b/.gitignore index b6c2fdc035..f381981b96 100644 --- a/.gitignore +++ b/.gitignore @@ -179,4 +179,4 @@ autogpt_platform/backend/settings.py .claude/settings.local.json # Auto generated client -autogpt_platform/frontend/src/api/__generated__ +autogpt_platform/frontend/src/app/api/__generated__ diff --git a/autogpt_platform/frontend/orval.config.ts b/autogpt_platform/frontend/orval.config.ts index afce23a441..296115d08e 100644 --- a/autogpt_platform/frontend/orval.config.ts +++ b/autogpt_platform/frontend/orval.config.ts @@ -3,13 +3,13 @@ import { defineConfig } from "orval"; export default defineConfig({ autogpt_api_client: { input: { - target: `./src/api/openapi.json`, + target: `./src/app/api/openapi.json`, override: { - transformer: "./src/api/transformers/fix-tags.mjs", + transformer: "./src/app/api/transformers/fix-tags.mjs", }, }, output: { - workspace: "./src/api", + workspace: "./src/app/api", target: `./__generated__/endpoints`, schemas: "./__generated__/models", mode: "tags-split", @@ -39,13 +39,13 @@ export default defineConfig({ }, autogpt_zod_schema: { input: { - target: `./src/api/openapi.json`, + target: `./src/app/api/openapi.json`, override: { - transformer: "./src/api/transformers/fix-tags.mjs", + transformer: "./src/app/api/transformers/fix-tags.mjs", }, }, output: { - workspace: "./src/api", + workspace: "./src/app/api", target: `./__generated__/zod-schema`, schemas: "./__generated__/models", mode: "tags-split", diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index df044a8166..de44ec141e 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -17,7 +17,8 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test-storybook": "test-storybook", - "fetch:openapi": "curl http://localhost:8006/openapi.json > ./src/api/openapi.json && prettier --write ./src/api/openapi.json", + "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm run build-storybook -- --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && pnpm run test-storybook\"", + "fetch:openapi": "curl http://localhost:8006/openapi.json > ./src/app/api/openapi.json && prettier --write ./src/app/api/openapi.json", "generate:api-client": "orval --config ./orval.config.ts", "generate:api-all": "pnpm run fetch:openapi && pnpm run generate:api-client" }, diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/useAPISection.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/useAPISection.tsx index 7c6c0ccf99..3e135c8b9c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/useAPISection.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeySection/useAPISection.tsx @@ -3,8 +3,8 @@ import { getGetV1ListUserApiKeysQueryKey, useDeleteV1RevokeApiKey, useGetV1ListUserApiKeys, -} from "@/api/__generated__/endpoints/api-keys/api-keys"; -import { APIKeyWithoutHash } from "@/api/__generated__/models/aPIKeyWithoutHash"; +} from "@/app/api/__generated__/endpoints/api-keys/api-keys"; +import { APIKeyWithoutHash } from "@/app/api/__generated__/models/aPIKeyWithoutHash"; import { useToast } from "@/components/ui/use-toast"; import { getQueryClient } from "@/lib/react-query/queryClient"; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/APIKeysModals.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/APIKeysModals.tsx index 1f53043bbd..04d6155936 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/APIKeysModals.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/APIKeysModals.tsx @@ -15,7 +15,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; import { useAPIkeysModals } from "./useAPIkeysModals"; -import { APIKeyPermission } from "@/api/__generated__/models/aPIKeyPermission"; +import { APIKeyPermission } from "@/app/api/__generated__/models/aPIKeyPermission"; export const APIKeysModals = () => { const { diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/useAPIkeysModals.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/useAPIkeysModals.tsx index 0d14d53241..6332205705 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/useAPIkeysModals.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/components/APIKeysModals/useAPIkeysModals.tsx @@ -2,9 +2,9 @@ import { getGetV1ListUserApiKeysQueryKey, usePostV1CreateNewApiKey, -} from "@/api/__generated__/endpoints/api-keys/api-keys"; -import { APIKeyPermission } from "@/api/__generated__/models/aPIKeyPermission"; -import { CreateAPIKeyResponse } from "@/api/__generated__/models/createAPIKeyResponse"; +} from "@/app/api/__generated__/endpoints/api-keys/api-keys"; +import { APIKeyPermission } from "@/app/api/__generated__/models/aPIKeyPermission"; +import { CreateAPIKeyResponse } from "@/app/api/__generated__/models/createAPIKeyResponse"; import { useToast } from "@/components/ui/use-toast"; import { getQueryClient } from "@/lib/react-query/queryClient"; import { useState } from "react"; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/actions.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/actions.ts index 0bf88e0b09..97c9d8b153 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/actions.ts @@ -6,7 +6,7 @@ import { NotificationPreferenceDTO } from "@/lib/autogpt-server-api/types"; import { postV1UpdateNotificationPreferences, postV1UpdateUserEmail, -} from "@/api/__generated__/endpoints/auth/auth"; +} from "@/app/api/__generated__/endpoints/auth/auth"; export async function updateSettings(formData: FormData) { const supabase = await getServerSupabase(); diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx index c4fbe84eb0..13734ad8de 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm.tsx @@ -13,7 +13,7 @@ import { import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Separator } from "@/components/ui/separator"; -import { NotificationPreference } from "@/api/__generated__/models/notificationPreference"; +import { NotificationPreference } from "@/app/api/__generated__/models/notificationPreference"; import { User } from "@supabase/supabase-js"; import { useSettingsForm } from "./useSettingsForm"; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/useSettingsForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/useSettingsForm.tsx index 72b2257406..4a297ee1a5 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/useSettingsForm.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/useSettingsForm.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { updateSettings } from "../../actions"; import { useToast } from "@/components/ui/use-toast"; -import { NotificationPreference } from "@/api/__generated__/models/notificationPreference"; +import { NotificationPreference } from "@/app/api/__generated__/models/notificationPreference"; import { User } from "@supabase/supabase-js"; export const useSettingsForm = ({ diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx index 34ad47fb66..8152999b5c 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { useGetV1GetNotificationPreferences } from "@/api/__generated__/endpoints/auth/auth"; +import { useGetV1GetNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth"; import { SettingsForm } from "@/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import * as React from "react"; diff --git a/autogpt_platform/frontend/src/api/mutators/custom-mutator.ts b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts similarity index 73% rename from autogpt_platform/frontend/src/api/mutators/custom-mutator.ts rename to autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts index e4d4e24afb..f0fdbc308c 100644 --- a/autogpt_platform/frontend/src/api/mutators/custom-mutator.ts +++ b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts @@ -1,7 +1,4 @@ -import { getSupabaseClient } from "@/lib/supabase/getSupabaseClient"; - -const BASE_URL = - process.env.NEXT_PUBLIC_AGPT_SERVER_BASE_URL || "http://localhost:8006"; +const BASE_URL = "/api/proxy"; // Sending request via nextjs Server const getBody = (c: Response | Request): Promise => { const contentType = c.headers.get("content-type"); @@ -17,18 +14,6 @@ const getBody = (c: Response | Request): Promise => { return c.text() as Promise; }; -const getSupabaseToken = async () => { - const supabase = await getSupabaseClient(); - - const { - data: { session }, - } = (await supabase?.auth.getSession()) || { - data: { session: null }, - }; - - return session?.access_token; -}; - export const customMutator = async ( url: string, options: RequestInit & { @@ -47,12 +32,6 @@ export const customMutator = async ( ...((requestOptions.headers as Record) || {}), }; - const token = await getSupabaseToken(); - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - const isFormData = data instanceof FormData; // Currently, only two content types are handled here: application/json and multipart/form-data diff --git a/autogpt_platform/frontend/src/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json similarity index 98% rename from autogpt_platform/frontend/src/api/openapi.json rename to autogpt_platform/frontend/src/app/api/openapi.json index 30b826fad5..00616f6fe9 100644 --- a/autogpt_platform/frontend/src/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -186,14 +186,16 @@ "oneOf": [ { "$ref": "#/components/schemas/OAuth2Credentials" }, { "$ref": "#/components/schemas/APIKeyCredentials" }, - { "$ref": "#/components/schemas/UserPasswordCredentials" } + { "$ref": "#/components/schemas/UserPasswordCredentials" }, + { "$ref": "#/components/schemas/HostScopedCredentials-Input" } ], "discriminator": { "propertyName": "type", "mapping": { "oauth2": "#/components/schemas/OAuth2Credentials", "api_key": "#/components/schemas/APIKeyCredentials", - "user_password": "#/components/schemas/UserPasswordCredentials" + "user_password": "#/components/schemas/UserPasswordCredentials", + "host_scoped": "#/components/schemas/HostScopedCredentials-Input" } }, "title": "Credentials" @@ -210,14 +212,18 @@ "oneOf": [ { "$ref": "#/components/schemas/OAuth2Credentials" }, { "$ref": "#/components/schemas/APIKeyCredentials" }, - { "$ref": "#/components/schemas/UserPasswordCredentials" } + { "$ref": "#/components/schemas/UserPasswordCredentials" }, + { + "$ref": "#/components/schemas/HostScopedCredentials-Output" + } ], "discriminator": { "propertyName": "type", "mapping": { "oauth2": "#/components/schemas/OAuth2Credentials", "api_key": "#/components/schemas/APIKeyCredentials", - "user_password": "#/components/schemas/UserPasswordCredentials" + "user_password": "#/components/schemas/UserPasswordCredentials", + "host_scoped": "#/components/schemas/HostScopedCredentials-Output" } }, "title": "Response Postv1Createcredentials" @@ -270,14 +276,18 @@ "oneOf": [ { "$ref": "#/components/schemas/OAuth2Credentials" }, { "$ref": "#/components/schemas/APIKeyCredentials" }, - { "$ref": "#/components/schemas/UserPasswordCredentials" } + { "$ref": "#/components/schemas/UserPasswordCredentials" }, + { + "$ref": "#/components/schemas/HostScopedCredentials-Output" + } ], "discriminator": { "propertyName": "type", "mapping": { "oauth2": "#/components/schemas/OAuth2Credentials", "api_key": "#/components/schemas/APIKeyCredentials", - "user_password": "#/components/schemas/UserPasswordCredentials" + "user_password": "#/components/schemas/UserPasswordCredentials", + "host_scoped": "#/components/schemas/HostScopedCredentials-Output" } }, "title": "Response Getv1Getcredential" @@ -434,9 +444,7 @@ "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_postV1LogRawMetric" - } + "schema": { "$ref": "#/components/schemas/LogRawMetricRequest" } } }, "required": true @@ -3797,16 +3805,6 @@ "required": ["type", "data", "data_index"], "title": "Body_postV1LogRawAnalytics" }, - "Body_postV1LogRawMetric": { - "properties": { - "metric_name": { "type": "string", "title": "Metric Name" }, - "metric_value": { "type": "number", "title": "Metric Value" }, - "data_string": { "type": "string", "title": "Data String" } - }, - "type": "object", - "required": ["metric_name", "metric_value", "data_string"], - "title": "Body_postV1LogRawMetric" - }, "Body_postV2Add_credits_to_user": { "properties": { "user_id": { "type": "string", "title": "User Id" }, @@ -4019,7 +4017,7 @@ "provider": { "$ref": "#/components/schemas/ProviderName" }, "type": { "type": "string", - "enum": ["api_key", "oauth2", "user_password"], + "enum": ["api_key", "oauth2", "user_password", "host_scoped"], "title": "Type" } }, @@ -4035,7 +4033,7 @@ "provider": { "type": "string", "title": "Provider" }, "type": { "type": "string", - "enum": ["api_key", "oauth2", "user_password"], + "enum": ["api_key", "oauth2", "user_password", "host_scoped"], "title": "Type" }, "title": { @@ -4052,6 +4050,11 @@ "username": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Username" + }, + "host": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Host", + "description": "Host pattern for host-scoped credentials" } }, "type": "object", @@ -4393,6 +4396,70 @@ "type": "object", "title": "HTTPValidationError" }, + "HostScopedCredentials-Input": { + "properties": { + "id": { "type": "string", "title": "Id" }, + "provider": { "type": "string", "title": "Provider" }, + "title": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Title" + }, + "type": { + "type": "string", + "const": "host_scoped", + "title": "Type", + "default": "host_scoped" + }, + "host": { + "type": "string", + "title": "Host", + "description": "The host/URI pattern to match against request URLs" + }, + "headers": { + "additionalProperties": { + "type": "string", + "format": "password", + "writeOnly": true + }, + "type": "object", + "title": "Headers", + "description": "Key-value header map to add to matching requests" + } + }, + "type": "object", + "required": ["provider", "host"], + "title": "HostScopedCredentials" + }, + "HostScopedCredentials-Output": { + "properties": { + "id": { "type": "string", "title": "Id" }, + "provider": { "type": "string", "title": "Provider" }, + "title": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Title" + }, + "type": { + "type": "string", + "const": "host_scoped", + "title": "Type", + "default": "host_scoped" + }, + "host": { + "type": "string", + "title": "Host", + "description": "The host/URI pattern to match against request URLs" + }, + "headers": { + "additionalProperties": { "type": "string" }, + "type": "object", + "title": "Headers", + "description": "Key-value header map to add to matching requests" + } + }, + "type": "object", + "required": ["provider", "host"], + "title": "HostScopedCredentials" + }, "LibraryAgent": { "properties": { "id": { "type": "string", "title": "Id" }, @@ -4712,6 +4779,24 @@ "required": ["source_id", "sink_id", "source_name", "sink_name"], "title": "Link" }, + "LogRawMetricRequest": { + "properties": { + "metric_name": { + "type": "string", + "minLength": 1, + "title": "Metric Name" + }, + "metric_value": { "type": "number", "title": "Metric Value" }, + "data_string": { + "type": "string", + "minLength": 1, + "title": "Data String" + } + }, + "type": "object", + "required": ["metric_name", "metric_value", "data_string"], + "title": "LogRawMetricRequest" + }, "LoginResponse": { "properties": { "login_url": { "type": "string", "title": "Login Url" }, @@ -5459,6 +5544,7 @@ "google", "google_maps", "groq", + "http", "hubspot", "ideogram", "jina", diff --git a/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts b/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts new file mode 100644 index 0000000000..dd959a31cd --- /dev/null +++ b/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts @@ -0,0 +1,153 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + makeAuthenticatedRequest, + makeAuthenticatedFileUpload, +} from "@/lib/autogpt-server-api/helpers"; + +const BACKEND_BASE_URL = + process.env.NEXT_PUBLIC_AGPT_SERVER_BASE_URL || "http://localhost:8006"; + +function buildBackendUrl(path: string[], queryString: string): string { + const backendPath = path.join("/"); + return `${BACKEND_BASE_URL}/${backendPath}${queryString}`; +} + +async function handleJsonRequest( + req: NextRequest, + method: string, + backendUrl: string, +): Promise { + const payload = await req.json(); + return await makeAuthenticatedRequest( + method, + backendUrl, + payload, + "application/json", + ); +} + +async function handleFormDataRequest( + req: NextRequest, + backendUrl: string, +): Promise { + const formData = await req.formData(); + return await makeAuthenticatedFileUpload(backendUrl, formData); +} + +async function handleUrlEncodedRequest( + req: NextRequest, + method: string, + backendUrl: string, +): Promise { + const textPayload = await req.text(); + const params = new URLSearchParams(textPayload); + const payload = Object.fromEntries(params.entries()); + return await makeAuthenticatedRequest( + method, + backendUrl, + payload, + "application/x-www-form-urlencoded", + ); +} + +async function handleRequestWithoutBody( + method: string, + backendUrl: string, +): Promise { + return await makeAuthenticatedRequest(method, backendUrl); +} + +function createUnsupportedContentTypeResponse( + contentType: string | null, +): NextResponse { + return NextResponse.json( + { + error: + "Unsupported Content-Type for proxying with authentication helpers.", + receivedContentType: contentType, + supportedContentTypes: [ + "application/json", + "multipart/form-data", + "application/x-www-form-urlencoded", + ], + }, + { status: 415 }, // Unsupported Media Type + ); +} + +function createResponse( + responseBody: any, + responseStatus: number, + responseHeaders: Record, +): NextResponse { + if (responseStatus === 204) { + return new NextResponse(null, { status: responseStatus }); + } else { + return NextResponse.json(responseBody, { + status: responseStatus, + headers: responseHeaders, + }); + } +} + +function createErrorResponse(error: unknown): NextResponse { + console.error("API proxy error:", error); + const detail = + error instanceof Error ? error.message : "An unknown error occurred"; + return NextResponse.json( + { error: "Proxy request failed", detail }, + { status: 500 }, // Internal Server Error + ); +} + +/** + * A simple proxy route that forwards requests to the backend API. + * It injects the server-side authentication token into the Authorization header. + * It uses the makeAuthenticatedRequest and makeAuthenticatedFileUpload helpers + * to handle request body parsing and authentication. + */ +async function handler( + req: NextRequest, + { params }: { params: Promise<{ path: string[] }> }, +) { + const { path } = await params; + const url = new URL(req.url); + const queryString = url.search; + const backendUrl = buildBackendUrl(path, queryString); + + const method = req.method; + const contentType = req.headers.get("Content-Type"); + + let responseBody: any; + const responseStatus: number = 200; + const responseHeaders: Record = { + "Content-Type": "application/json", + }; + + try { + if (method === "GET" || method === "DELETE") { + responseBody = await handleRequestWithoutBody(method, backendUrl); + } else if (contentType?.includes("application/json")) { + responseBody = await handleJsonRequest(req, method, backendUrl); + } else if (contentType?.includes("multipart/form-data")) { + responseBody = await handleFormDataRequest(req, backendUrl); + responseHeaders["Content-Type"] = "text/plain"; + } else if (contentType?.includes("application/x-www-form-urlencoded")) { + responseBody = await handleUrlEncodedRequest(req, method, backendUrl); + } else { + return createUnsupportedContentTypeResponse(contentType); + } + + return createResponse(responseBody, responseStatus, responseHeaders); + } catch (error) { + return createErrorResponse(error); + } +} + +export { + handler as GET, + handler as POST, + handler as PUT, + handler as PATCH, + handler as DELETE, +}; diff --git a/autogpt_platform/frontend/src/api/transformers/fix-tags.mjs b/autogpt_platform/frontend/src/app/api/transformers/fix-tags.mjs similarity index 100% rename from autogpt_platform/frontend/src/api/transformers/fix-tags.mjs rename to autogpt_platform/frontend/src/app/api/transformers/fix-tags.mjs diff --git a/autogpt_platform/frontend/src/lib/supabase/getSupabaseClient.ts b/autogpt_platform/frontend/src/lib/supabase/getSupabaseClient.ts deleted file mode 100644 index 055e31f394..0000000000 --- a/autogpt_platform/frontend/src/lib/supabase/getSupabaseClient.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; -import { createBrowserClient } from "@supabase/ssr"; - -const isClient = typeof window !== "undefined"; - -export const getSupabaseClient = async () => { - return isClient - ? createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { isSingleton: true }, - ) - : await getServerSupabase(); -};