diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index bc8f01a80..b2fec9292 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -9,15 +9,17 @@ import { INTEGRATION_CHECKLY_API_URL, INTEGRATION_CIRCLECI, INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_FLYIO, INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, - INTEGRATION_CLOUDFLARE_PAGES, - INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_GITLAB_API_URL, INTEGRATION_HEROKU, INTEGRATION_HEROKU_API_URL, + INTEGRATION_LARAVELFORGE, + INTEGRATION_LARAVELFORGE_API_URL, INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RAILWAY, @@ -116,6 +118,12 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_LARAVELFORGE: + apps = await getAppsLaravelForge({ + accessToken, + serverId: accessId + }); + break; case INTEGRATION_TRAVISCI: apps = await getAppsTravisCI({ accessToken, @@ -398,6 +406,40 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { return apps; }; +/** + * Return list of sites for Laravel Forge integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Laravel Forge API + * @param {String} obj.serverId - server id of Laravel Forge + * @returns {Object[]} apps - names and ids of Laravel Forge sites + * @returns {String} apps.name - name of Laravel Forge sites + * @returns {String} apps.appId - id of Laravel Forge sites + */ +const getAppsLaravelForge = async ({ + accessToken, + serverId +}: { + accessToken: string; + serverId?: string; +}) => { + const res = ( + await standardRequest.get(`${INTEGRATION_LARAVELFORGE_API_URL}/api/v1/servers/${serverId}/sites`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Content-Type": "application/json", + }, + }) + ).data.sites; + + const apps = res.map((a: any) => ({ + name: a.name, + appId: a.id, + })); + + return apps; +}; + /** * Return list of apps for Fly.io integration * @param {Object} obj diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d94cc9faa..c4021977d 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -5,16 +5,17 @@ import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_CHECKLY, INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, INTEGRATION_HEROKU, + INTEGRATION_LARAVELFORGE, INTEGRATION_NETLIFY, INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, - INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, } from "../variables"; @@ -48,6 +49,7 @@ export interface IIntegration { | "railway" | "flyio" | "circleci" + | "laravel-forge" | "travisci" | "supabase" | "checkly" @@ -136,6 +138,7 @@ const integrationSchema = new Schema( INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 2d56c2063..4cc7ef530 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -7,16 +7,17 @@ import { INTEGRATION_AWS_SECRET_MANAGER, INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_CIRCLECI, + INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, INTEGRATION_HEROKU, + INTEGRATION_LARAVELFORGE, INTEGRATION_NETLIFY, INTEGRATION_RAILWAY, INTEGRATION_RENDER, INTEGRATION_SUPABASE, - INTEGRATION_CLOUDFLARE_PAGES, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, } from "../variables"; @@ -24,7 +25,7 @@ import { export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly' | 'cloudflare-pages'; + integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'laravel-forge' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly' | 'cloudflare-pages'; teamId: string; accountId: string; url: string; @@ -65,6 +66,7 @@ const integrationAuthSchema = new Schema( INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_HASHICORP_VAULT, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index c87d2eb97..18eaeb049 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -19,12 +19,13 @@ export const INTEGRATION_GITLAB = "gitlab"; export const INTEGRATION_RENDER = "render"; export const INTEGRATION_RAILWAY = "railway"; export const INTEGRATION_FLYIO = "flyio"; +export const INTEGRATION_LARAVELFORGE = "laravel-forge" export const INTEGRATION_CIRCLECI = "circleci"; export const INTEGRATION_TRAVISCI = "travisci"; -export const INTEGRATION_SUPABASE = 'supabase'; -export const INTEGRATION_CHECKLY = 'checkly'; -export const INTEGRATION_HASHICORP_VAULT = 'hashicorp-vault'; -export const INTEGRATION_CLOUDFLARE_PAGES = 'cloudflare-pages'; +export const INTEGRATION_SUPABASE = "supabase"; +export const INTEGRATION_CHECKLY = "checkly"; +export const INTEGRATION_HASHICORP_VAULT = "hashicorp-vault"; +export const INTEGRATION_CLOUDFLARE_PAGES = "cloudflare-pages"; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -35,6 +36,7 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, + INTEGRATION_LARAVELFORGE, INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, @@ -65,9 +67,10 @@ export const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphq export const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; -export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; -export const INTEGRATION_CHECKLY_API_URL = 'https://api.checklyhq.com'; -export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = 'https://api.cloudflare.com'; +export const INTEGRATION_SUPABASE_API_URL = "https://api.supabase.com"; +export const INTEGRATION_LARAVELFORGE_API_URL = "https://forge.laravel.com"; +export const INTEGRATION_CHECKLY_API_URL = "https://api.checklyhq.com"; +export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com"; export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ @@ -144,6 +147,15 @@ export const getIntegrationOptions = async () => { clientId: "", docsLink: "", }, + { + name: "Laravel Forge", + slug: "laravel-forge", + image: "Laravel Forge.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, { name: "AWS Secret Manager", slug: "aws-secret-manager", @@ -221,20 +233,20 @@ export const getIntegrationOptions = async () => { slug: "gcp", image: "Google Cloud Platform.png", isAvailable: false, - type: '', - clientId: '', - docsLink: '' + type: "", + clientId: "", + docsLink: "" }, { - name: 'Cloudflare Pages', - slug: 'cloudflare-pages', - image: 'Cloudflare.png', + name: "Cloudflare Pages", + slug: "cloudflare-pages", + image: "Cloudflare.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "" } ] return INTEGRATION_OPTIONS; -} \ No newline at end of file +} diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 480061e1a..c3c74091d 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -12,6 +12,7 @@ const integrationSlugNameMapping: Mapping = { 'github': 'GitHub', 'gitlab': 'GitLab', 'render': 'Render', + 'laravel-forge': "Laravel Forge", 'railway': 'Railway', 'flyio': 'Fly.io', 'circleci': 'CircleCI', diff --git a/frontend/public/images/integrations/Laravel Forge.png b/frontend/public/images/integrations/Laravel Forge.png new file mode 100644 index 000000000..a91139004 Binary files /dev/null and b/frontend/public/images/integrations/Laravel Forge.png differ diff --git a/frontend/src/pages/integrations/laravel-forge/authorize.tsx b/frontend/src/pages/integrations/laravel-forge/authorize.tsx new file mode 100644 index 000000000..db47dc250 --- /dev/null +++ b/frontend/src/pages/integrations/laravel-forge/authorize.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; + +import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; +import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; + +export default function LaravelForgeCreateIntegrationPage() { + const router = useRouter(); + const [apiKey, setApiKey] = useState(""); + const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [serverId, setServerId] = useState(""); + const [serverIdErrorText, setServerIdErrorText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(""); + setServerIdErrorText(""); + + if (apiKey.length === 0 || serverId.length === 0) { + if (apiKey.length === 0) setApiKeyErrorText("Access Token cannot be blank"); + if (serverId.length === 0) setServerIdErrorText("Server Id cannot be blank"); + return; + } + + setIsLoading(true); + + const integrationAuth = await saveIntegrationAccessToken({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "laravel-forge", + accessId: serverId, + accessToken: apiKey, + url: null, + namespace: null + }); + + setIsLoading(false); + + router.push(`/integrations/laravel-forge/create?integrationAuthId=${integrationAuth._id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + Laravel Forge Integration + + setApiKey(e.target.value)} /> + + + setServerId(e.target.value)} /> + + + +
+ ); +} + +LaravelForgeCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/laravel-forge/create.tsx b/frontend/src/pages/integrations/laravel-forge/create.tsx new file mode 100644 index 000000000..acfeb6932 --- /dev/null +++ b/frontend/src/pages/integrations/laravel-forge/create.tsx @@ -0,0 +1,155 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import queryString from "query-string"; + +import { + Button, + Card, + CardTitle, + FormControl, + Input, + Select, + SelectItem +} from "../../../components/v2"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthById +} from "../../../hooks/api/integrationAuth"; +import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +import createIntegration from "../../api/integrations/createIntegration"; + +export default function LaravelForgeCreateIntegrationPage() { + const router = useRouter(); + + const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); + const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); + const { data: integrationAuthApps } = useGetIntegrationAuthApps({ + integrationAuthId: (integrationAuthId as string) ?? "" + }); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [targetApp, setTargetApp] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetApp(integrationAuthApps[0].name); + } else { + setTargetApp("none"); + } + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?._id) return; + + setIsLoading(true); + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp, + appId: + integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) + ?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + targetEnvironmentId: null, + targetService: null, + targetServiceId: null, + owner: null, + path: null, + region: null, + secretPath + }); + + setIsLoading(false); + + router.push(`/integrations/${localStorage.getItem("projectData.id")}`); + } catch (err) { + console.error(err); + } + }; + + return integrationAuth && + workspace && + selectedSourceEnvironment && + integrationAuthApps && + targetApp ? ( +
+ + Laravel Forge Integration + + + + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> + + + + + + +
+ ) : ( +
+ ); +} + +LaravelForgeCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index fc60ead6d..fd82d1fa7 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -71,6 +71,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "circleci": link = `${window.location.origin}/integrations/circleci/authorize`; break; + case "laravel-forge": + link = `${window.location.origin}/integrations/laravel-forge/authorize`; + break; case "travisci": link = `${window.location.origin}/integrations/travisci/authorize`; break;