diff --git a/api/server/handlers/datastore/update.go b/api/server/handlers/datastore/update.go index 7a2a335387..7fc92d1455 100644 --- a/api/server/handlers/datastore/update.go +++ b/api/server/handlers/datastore/update.go @@ -190,6 +190,9 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques case "NEON": datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_NEON datastoreProto.KindValues = &porterv1.ManagedDatastore_NeonKind{} + case "UPSTASH": + datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_UPSTASH + datastoreProto.KindValues = &porterv1.ManagedDatastore_UpstashKind{} default: err = telemetry.Error(ctx, span, nil, "invalid datastore type") h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) diff --git a/api/server/handlers/neon_integration/list.go b/api/server/handlers/neon_integration/list.go index e250e6e587..f6f1dddc32 100644 --- a/api/server/handlers/neon_integration/list.go +++ b/api/server/handlers/neon_integration/list.go @@ -13,7 +13,7 @@ import ( "github.com/porter-dev/porter/internal/telemetry" ) -// ListNeonIntegrationsHandler is a struct for listing all noen integrations for a given project +// ListNeonIntegrationsHandler is a struct for listing all neon integrations for a given project type ListNeonIntegrationsHandler struct { handlers.PorterHandlerReadWriter } diff --git a/api/server/handlers/oauth_callback/upstash.go b/api/server/handlers/oauth_callback/upstash.go index 97622b9df9..3907eb9efc 100644 --- a/api/server/handlers/oauth_callback/upstash.go +++ b/api/server/handlers/oauth_callback/upstash.go @@ -111,6 +111,7 @@ func (p *OAuthCallbackUpstashHandler) ServeHTTP(w http.ResponseWriter, r *http.R oauthInt := integrations.UpstashIntegration{ SharedOAuthModel: integrations.SharedOAuthModel{ + ClientID: []byte(p.Config().UpstashConf.ClientID), AccessToken: []byte(token.AccessToken), RefreshToken: []byte(token.RefreshToken), Expiry: token.Expiry, diff --git a/api/server/handlers/upstash_integration/list.go b/api/server/handlers/upstash_integration/list.go new file mode 100644 index 0000000000..20c4f79ad8 --- /dev/null +++ b/api/server/handlers/upstash_integration/list.go @@ -0,0 +1,69 @@ +package upstash_integration + +import ( + "net/http" + "time" + + "github.com/porter-dev/porter/api/server/handlers" + "github.com/porter-dev/porter/api/server/shared" + "github.com/porter-dev/porter/api/server/shared/apierrors" + "github.com/porter-dev/porter/api/server/shared/config" + "github.com/porter-dev/porter/api/types" + "github.com/porter-dev/porter/internal/models" + "github.com/porter-dev/porter/internal/telemetry" +) + +// ListUpstashIntegrationsHandler is a struct for listing all upstash integrations for a given project +type ListUpstashIntegrationsHandler struct { + handlers.PorterHandlerReadWriter +} + +// NewListUpstashIntegrationsHandler constructs a ListUpstashIntegrationsHandler +func NewListUpstashIntegrationsHandler( + config *config.Config, + decoderValidator shared.RequestDecoderValidator, + writer shared.ResultWriter, +) *ListUpstashIntegrationsHandler { + return &ListUpstashIntegrationsHandler{ + PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer), + } +} + +// UpstashIntegration describes a upstash integration +type UpstashIntegration struct { + CreatedAt time.Time `json:"created_at"` +} + +// ListUpstashIntegrationsResponse describes the list upstash integrations response body +type ListUpstashIntegrationsResponse struct { + // Integrations is a list of upstash integrations + Integrations []UpstashIntegration `json:"integrations"` +} + +// ServeHTTP returns a list of upstash integrations associated with the specified project +func (h *ListUpstashIntegrationsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx, span := telemetry.NewSpan(r.Context(), "serve-list-upstash-integrations") + defer span.End() + + project, _ := ctx.Value(types.ProjectScope).(*models.Project) + + resp := ListUpstashIntegrationsResponse{} + integrationList := make([]UpstashIntegration, 0) + + integrations, err := h.Repo().UpstashIntegration().Integrations(ctx, project.ID) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting datastores") + h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + for _, int := range integrations { + integrationList = append(integrationList, UpstashIntegration{ + CreatedAt: int.CreatedAt, + }) + } + + resp.Integrations = integrationList + + h.WriteResult(w, r, resp) +} diff --git a/api/server/router/project.go b/api/server/router/project.go index 2b30b03103..5a922f33bb 100644 --- a/api/server/router/project.go +++ b/api/server/router/project.go @@ -5,6 +5,7 @@ import ( "github.com/porter-dev/porter/api/server/handlers/cloud_provider" "github.com/porter-dev/porter/api/server/handlers/neon_integration" + "github.com/porter-dev/porter/api/server/handlers/upstash_integration" "github.com/porter-dev/porter/api/server/handlers/deployment_target" @@ -2049,5 +2050,32 @@ func getProjectRoutes( Router: r, }) + // GET /api/projects/{project_id}/upstash-integrations -> apiContract.NewListUpstashIntegrationsHandler + listUpstashIntegrationsEndpoint := factory.NewAPIEndpoint( + &types.APIRequestMetadata{ + Verb: types.APIVerbGet, + Method: types.HTTPVerbGet, + Path: &types.Path{ + Parent: basePath, + RelativePath: relPath + "/upstash-integrations", + }, + Scopes: []types.PermissionScope{ + types.UserScope, + types.ProjectScope, + }, + }, + ) + + listUpstashIntegrationsHandler := upstash_integration.NewListUpstashIntegrationsHandler( + config, + factory.GetDecoderValidator(), + factory.GetResultWriter(), + ) + routes = append(routes, &router.Route{ + Endpoint: listUpstashIntegrationsEndpoint, + Handler: listUpstashIntegrationsHandler, + Router: r, + }) + return routes, newPath } diff --git a/dashboard/src/assets/upstash.svg b/dashboard/src/assets/upstash.svg new file mode 100644 index 0000000000..e928467552 --- /dev/null +++ b/dashboard/src/assets/upstash.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/dashboard/src/lib/databases/types.ts b/dashboard/src/lib/databases/types.ts index 0d49a168a3..84fb81aa13 100644 --- a/dashboard/src/lib/databases/types.ts +++ b/dashboard/src/lib/databases/types.ts @@ -37,6 +37,7 @@ const datastoreTypeValidator = z.enum([ "MANAGED_REDIS", "MANAGED_POSTGRES", "NEON", + "UPSTASH", ]); const datastoreEngineValidator = z.enum([ "UNKNOWN", @@ -114,6 +115,10 @@ export const DATASTORE_TYPE_NEON: DatastoreType = { name: "NEON" as const, displayName: "Neon", }; +export const DATASTORE_TYPE_UPSTASH: DatastoreType = { + name: "UPSTASH" as const, + displayName: "Upstash", +}; export type DatastoreState = { state: z.infer["status"]; @@ -334,6 +339,10 @@ const neonValidator = z.object({ type: z.literal("neon"), }); +const upstashValidator = z.object({ + type: z.literal("upstash"), +}); + export const dbFormValidator = z.object({ name: z .string() @@ -355,6 +364,7 @@ export const dbFormValidator = z.object({ managedRedisConfigValidator, managedPostgresConfigValidator, neonValidator, + upstashValidator, ]), clusterId: z.number(), }); diff --git a/dashboard/src/lib/hooks/useDatastore.ts b/dashboard/src/lib/hooks/useDatastore.ts index 9031ca5a37..2401aab029 100644 --- a/dashboard/src/lib/hooks/useDatastore.ts +++ b/dashboard/src/lib/hooks/useDatastore.ts @@ -21,7 +21,13 @@ type DatastoreHook = { }; type CreateDatastoreInput = { name: string; - type: "RDS" | "ELASTICACHE" | "MANAGED-POSTGRES" | "MANAGED-REDIS" | "NEON"; + type: + | "RDS" + | "ELASTICACHE" + | "MANAGED-POSTGRES" + | "MANAGED-REDIS" + | "NEON" + | "UPSTASH"; engine: "POSTGRES" | "AURORA-POSTGRES" | "REDIS"; values: object; }; @@ -147,6 +153,19 @@ const clientDbToCreateInput = (values: DbFormData): CreateDatastoreInput => { engine: "POSTGRES", }) ) + .with( + { config: { type: "upstash" } }, + (values): CreateDatastoreInput => ({ + name: values.name, + values: { + config: { + name: values.name, + }, + }, + type: "UPSTASH", + engine: "REDIS", + }) + ) .exhaustive(); }; diff --git a/dashboard/src/lib/hooks/useUpstash.ts b/dashboard/src/lib/hooks/useUpstash.ts new file mode 100644 index 0000000000..5945a323cd --- /dev/null +++ b/dashboard/src/lib/hooks/useUpstash.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +import { + upstashIntegrationValidator, + type ClientUpstashIntegration, +} from "lib/upstash/types"; + +import api from "shared/api"; + +type TUseUpstash = { + getUpstashIntegrations: ({ + projectId, + }: { + projectId: number; + }) => Promise; +}; +export const useUpstash = (): TUseUpstash => { + const getUpstashIntegrations = async ({ + projectId, + }: { + projectId: number; + }): Promise => { + const response = await api.getUpstashIntegrations( + "", + {}, + { + projectId, + } + ); + + const results = await z + .object({ integrations: z.array(upstashIntegrationValidator) }) + .parseAsync(response.data); + + return results.integrations; + }; + + return { + getUpstashIntegrations, + }; +}; diff --git a/dashboard/src/lib/upstash/types.ts b/dashboard/src/lib/upstash/types.ts new file mode 100644 index 0000000000..6aa578ec27 --- /dev/null +++ b/dashboard/src/lib/upstash/types.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const upstashIntegrationValidator = z.object({ + created_at: z.string(), +}); +export type ClientUpstashIntegration = z.infer< + typeof upstashIntegrationValidator +>; diff --git a/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx b/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx index d2cba34e90..34f3838a3f 100644 --- a/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx +++ b/dashboard/src/main/home/database-dashboard/DatastoreFormContextProvider.tsx @@ -12,10 +12,14 @@ import { useDatastoreList } from "lib/hooks/useDatabaseList"; import { useDatastore } from "lib/hooks/useDatastore"; import { useIntercom } from "lib/hooks/useIntercom"; import { useNeon } from "lib/hooks/useNeon"; +import { useUpstash } from "lib/hooks/useUpstash"; import { Context } from "shared/Context"; -import NeonIntegrationModal from "./shared/NeonIntegrationModal"; +import { + NeonIntegrationModal, + UpstashIntegrationModal, +} from "./shared/NeonIntegrationModal"; // todo(ianedwards): refactor button to use more predictable state export type UpdateDatastoreButtonProps = { @@ -53,8 +57,11 @@ const DatastoreFormContextProvider: React.FC< const [updateDatastoreError, setUpdateDatastoreError] = useState(""); const { getNeonIntegrations } = useNeon(); + const { getUpstashIntegrations } = useUpstash(); const [showNeonIntegrationModal, setShowNeonIntegrationModal] = useState(false); + const [showUpstashIntegrationModal, setShowUpstashIntegrationModal] = + useState(false); const { showIntercomWithMessage } = useIntercom(); @@ -117,6 +124,16 @@ const DatastoreFormContextProvider: React.FC< return; } } + if (data.config.type === "upstash") { + const integrations = await getUpstashIntegrations({ + projectId: currentProject.id, + }); + if (integrations.length === 0) { + setShowUpstashIntegrationModal(true); + return; + } + } + await createDatastore(data); history.push(`/datastores/${data.name}`); } catch (err) { @@ -154,6 +171,13 @@ const DatastoreFormContextProvider: React.FC< }} /> )} + {showUpstashIntegrationModal && ( + { + setShowUpstashIntegrationModal(false); + }} + /> + )} ); }; diff --git a/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx b/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx index 30c8783ef8..6e114a8dea 100644 --- a/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx +++ b/dashboard/src/main/home/database-dashboard/DatastoreProvisioningIndicator.tsx @@ -2,7 +2,10 @@ import React, { useMemo } from "react"; import StatusBar from "components/porter/StatusBar"; -import { DATASTORE_TEMPLATE_NEON } from "./constants"; +import { + DATASTORE_TEMPLATE_NEON, + DATASTORE_TEMPLATE_UPSTASH, +} from "./constants"; import { useDatastoreContext } from "./DatabaseContextProvider"; const DatastoreProvisioningIndicator: React.FC = () => { @@ -40,9 +43,13 @@ const DatastoreProvisioningIndicator: React.FC = () => { return { percentCompleted, title, titleDescriptor, isCreating }; }, [datastore]); + // TODO: refactor this so the template has the setup/deletion time const subtitle = useMemo(() => { return `${isCreating ? "Setup" : "Deletion"} can take up to ${ - datastore.template === DATASTORE_TEMPLATE_NEON ? 5 : 20 + datastore.template === DATASTORE_TEMPLATE_NEON || + datastore.template === DATASTORE_TEMPLATE_UPSTASH + ? 5 + : 20 } minutes. You can close this window and come back later.`; }, [datastore]); diff --git a/dashboard/src/main/home/database-dashboard/constants.ts b/dashboard/src/main/home/database-dashboard/constants.ts index 6aed92c2e8..e0584a9976 100644 --- a/dashboard/src/main/home/database-dashboard/constants.ts +++ b/dashboard/src/main/home/database-dashboard/constants.ts @@ -15,6 +15,7 @@ import { DATASTORE_TYPE_MANAGED_REDIS, DATASTORE_TYPE_NEON, DATASTORE_TYPE_RDS, + DATASTORE_TYPE_UPSTASH, type DatastoreEngine, type DatastoreTemplate, } from "lib/databases/types"; @@ -25,6 +26,7 @@ import infra from "assets/cluster.svg"; import neon from "assets/neon.svg"; import postgresql from "assets/postgresql.svg"; import redis from "assets/redis.svg"; +import upstash from "assets/upstash.svg"; import ConfigurationTab from "./tabs/ConfigurationTab"; import ConnectTab from "./tabs/ConnectTab"; @@ -418,6 +420,41 @@ export const DATASTORE_TEMPLATE_NEON: DatastoreTemplate = Object.freeze({ ], }); +export const DATASTORE_TEMPLATE_UPSTASH: DatastoreTemplate = Object.freeze({ + name: "Upstash", + displayName: "Upstash", + highLevelType: DATASTORE_ENGINE_REDIS, + type: DATASTORE_TYPE_UPSTASH, + engine: DATASTORE_ENGINE_REDIS, + supportedEngineVersions: [], + icon: upstash as string, + description: + "A fully managed serverless Postgres. Neon separates storage and compute to offer autoscaling, branching, and bottomless storage.", + disabled: true, + instanceTiers: [], + creationStateProgression: [ + DATASTORE_STATE_CREATING, + DATASTORE_STATE_AVAILABLE, + ], + deletionStateProgression: [ + DATASTORE_STATE_AWAITING_DELETION, + DATASTORE_STATE_DELETING_RECORD, + DATASTORE_STATE_DELETED, + ], + tabs: [ + { + name: "connectivity", + displayName: "Connectivity", + component: PublicDatastoreConnectTab, + }, + { + name: "settings", + displayName: "Settings", + component: SettingsTab, + }, + ], +}); + export const SUPPORTED_DATASTORE_TEMPLATES: DatastoreTemplate[] = [ DATASTORE_TEMPLATE_AWS_RDS, DATASTORE_TEMPLATE_AWS_AURORA, @@ -425,4 +462,5 @@ export const SUPPORTED_DATASTORE_TEMPLATES: DatastoreTemplate[] = [ DATASTORE_TEMPLATE_MANAGED_POSTGRES, DATASTORE_TEMPLATE_MANAGED_REDIS, DATASTORE_TEMPLATE_NEON, + DATASTORE_TEMPLATE_UPSTASH, ]; diff --git a/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx b/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx index 70590cf62d..36ae1b1dd3 100644 --- a/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx +++ b/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx @@ -24,6 +24,7 @@ import { DATASTORE_ENGINE_POSTGRES, DATASTORE_ENGINE_REDIS, DATASTORE_TEMPLATE_NEON, + DATASTORE_TEMPLATE_UPSTASH, SUPPORTED_DATASTORE_TEMPLATES, } from "../constants"; import { useDatastoreFormContext } from "../DatastoreFormContextProvider"; @@ -48,21 +49,14 @@ const SandboxDatastoreForm: React.FC = () => { const { updateDatastoreButtonProps } = useDatastoreFormContext(); const availableEngines: BlockSelectOption[] = useMemo(() => { - return [ - DATASTORE_ENGINE_POSTGRES, - { - ...DATASTORE_ENGINE_REDIS, - disabledOpts: { - tooltipText: "Coming soon!", - }, - }, - ]; + return [DATASTORE_ENGINE_POSTGRES, DATASTORE_ENGINE_REDIS]; }, [watchClusterId]); const availableHostTypes: BlockSelectOption[] = useMemo(() => { - const options = [DATASTORE_TEMPLATE_NEON].filter( - (t) => t.highLevelType.name === watchEngine - ); + const options = [ + DATASTORE_TEMPLATE_NEON, + DATASTORE_TEMPLATE_UPSTASH, + ].filter((t) => t.highLevelType.name === watchEngine); return options; }, [watchEngine]); diff --git a/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx index 750da78703..0e64e6d1bb 100644 --- a/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx +++ b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx @@ -13,6 +13,7 @@ import { DATASTORE_TYPE_MANAGED_REDIS, DATASTORE_TYPE_NEON, DATASTORE_TYPE_RDS, + DATASTORE_TYPE_UPSTASH, type DatastoreConnectionInfo, type DatastoreTemplate, } from "lib/databases/types"; @@ -39,6 +40,11 @@ const ConnectionInfo: React.FC = ({ connectionInfo, template }) => { () => `rediss://:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/0?ssl_cert_reqs=CERT_REQUIRED` ) + .with( + { type: DATASTORE_TYPE_UPSTASH }, + () => + `rediss://default:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/0` + ) .with( { type: DATASTORE_TYPE_MANAGED_REDIS }, () => diff --git a/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx b/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx index 3c0118efb8..ec5199401d 100644 --- a/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx +++ b/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx @@ -7,6 +7,7 @@ import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import { useAuthWindow } from "lib/hooks/useAuthWindow"; import { useNeon } from "lib/hooks/useNeon"; +import { useUpstash } from "lib/hooks/useUpstash"; import { useDatastoreFormContext } from "../DatastoreFormContextProvider"; @@ -14,7 +15,7 @@ type Props = { onClose: () => void; }; -const NeonIntegrationModal: React.FC = ({ onClose }) => { +export const NeonIntegrationModal: React.FC = ({ onClose }) => { const { projectId } = useDatastoreFormContext(); const { getNeonIntegrations } = useNeon(); const { openAuthWindow } = useAuthWindow({ @@ -57,4 +58,45 @@ const NeonIntegrationModal: React.FC = ({ onClose }) => { ); }; -export default NeonIntegrationModal; +export const UpstashIntegrationModal: React.FC = ({ onClose }) => { + const { projectId } = useDatastoreFormContext(); + const { getUpstashIntegrations } = useUpstash(); + const { openAuthWindow } = useAuthWindow({ + authUrl: `/api/projects/${projectId}/oauth/upstash`, + }); + + const upstashIntegrationsResp = useQuery( + ["getUpstashIntegrations", projectId], + async () => { + const integrations = await getUpstashIntegrations({ + projectId, + }); + return integrations; + }, + { + enabled: !!projectId, + refetchInterval: 1000, + } + ); + useEffect(() => { + if ( + upstashIntegrationsResp.isSuccess && + upstashIntegrationsResp.data.length > 0 + ) { + onClose(); + } + }, [upstashIntegrationsResp]); + + return ( + + Integrate Upstash + + + To continue, you must authenticate with Upstash.{" "} + + Authorize Porter to create Upstash datastores on your behalf + + + + ); +}; diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index a8f19ccc25..da73c5a746 100644 --- a/dashboard/src/shared/api.tsx +++ b/dashboard/src/shared/api.tsx @@ -2068,6 +2068,13 @@ const getNeonIntegrations = baseApi<{}, { projectId: number }>( } ); +const getUpstashIntegrations = baseApi<{}, { projectId: number }>( + "GET", + ({ projectId }) => { + return `/api/projects/${projectId}/upstash-integrations`; + } +); + const getRevisions = baseApi< {}, { id: number; cluster_id: number; namespace: string; name: string } @@ -2889,7 +2896,13 @@ const getDatastoreCredential = baseApi< const updateDatastore = baseApi< { name: string; - type: "RDS" | "ELASTICACHE" | "MANAGED-POSTGRES" | "MANAGED-REDIS" | "NEON"; + type: + | "RDS" + | "ELASTICACHE" + | "MANAGED-POSTGRES" + | "MANAGED-REDIS" + | "NEON" + | "UPSTASH"; engine: "POSTGRES" | "AURORA-POSTGRES" | "REDIS"; values: any; @@ -3895,6 +3908,7 @@ export default { getRepoIntegrations, getSlackIntegrations, getNeonIntegrations, + getUpstashIntegrations, getRepos, getRevisions, getTemplateInfo, diff --git a/go.mod b/go.mod index 73ab45ba82..4124f28a99 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/porter-dev/porter go 1.20 +// replace api-contracts with local +replace github.com/porter-dev/api-contracts => ../api-contracts + require ( cloud.google.com/go v0.110.2 // indirect github.com/AlecAivazis/survey/v2 v2.2.9 diff --git a/internal/repository/gorm/upstash.go b/internal/repository/gorm/upstash.go index 2e39b5b67a..34b3b04192 100644 --- a/internal/repository/gorm/upstash.go +++ b/internal/repository/gorm/upstash.go @@ -42,6 +42,31 @@ func (repo *UpstashIntegrationRepository) Insert( return created, nil } +// Integrations returns all upstash integrations for a given project +func (repo *UpstashIntegrationRepository) Integrations( + ctx context.Context, projectID uint, +) ([]ints.UpstashIntegration, error) { + ctx, span := telemetry.NewSpan(ctx, "gorm-list-upstash-integrations") + defer span.End() + + var integrations []ints.UpstashIntegration + + if err := repo.db.Where("project_id = ?", projectID).Find(&integrations).Error; err != nil { + return integrations, telemetry.Error(ctx, span, err, "failed to list upstash integrations") + } + + for i, integration := range integrations { + decrypted, err := repo.DecryptUpstashIntegration(integration, repo.key) + if err != nil { + return integrations, telemetry.Error(ctx, span, err, "failed to decrypt") + } + + integrations[i] = decrypted + } + + return integrations, nil +} + // EncryptUpstashIntegration will encrypt the upstash integration data before // writing to the DB func (repo *UpstashIntegrationRepository) EncryptUpstashIntegration( @@ -88,3 +113,50 @@ func (repo *UpstashIntegrationRepository) EncryptUpstashIntegration( return encrypted, nil } + +// DecryptUpstashIntegration will decrypt the upstash integration data before +// returning it from the DB +func (repo *UpstashIntegrationRepository) DecryptUpstashIntegration( + upstashInt ints.UpstashIntegration, + key *[32]byte, +) (ints.UpstashIntegration, error) { + decrypted := upstashInt + + if len(decrypted.ClientID) > 0 { + plaintext, err := encryption.Decrypt(decrypted.ClientID, key) + if err != nil { + return decrypted, err + } + + decrypted.ClientID = plaintext + } + + if len(decrypted.AccessToken) > 0 { + plaintext, err := encryption.Decrypt(decrypted.AccessToken, key) + if err != nil { + return decrypted, err + } + + decrypted.AccessToken = plaintext + } + + if len(decrypted.RefreshToken) > 0 { + plaintext, err := encryption.Decrypt(decrypted.RefreshToken, key) + if err != nil { + return decrypted, err + } + + decrypted.RefreshToken = plaintext + } + + if len(decrypted.DeveloperApiKey) > 0 { + plaintext, err := encryption.Decrypt(decrypted.DeveloperApiKey, key) + if err != nil { + return decrypted, err + } + + decrypted.DeveloperApiKey = plaintext + } + + return decrypted, nil +} diff --git a/internal/repository/upstash.go b/internal/repository/upstash.go index 18b4629579..56fc00ef16 100644 --- a/internal/repository/upstash.go +++ b/internal/repository/upstash.go @@ -10,4 +10,6 @@ import ( type UpstashIntegrationRepository interface { // Insert creates a new upstash integration Insert(ctx context.Context, upstashInt ints.UpstashIntegration) (ints.UpstashIntegration, error) + // Integrations returns all upstash integrations belonging to a project + Integrations(ctx context.Context, projectID uint) ([]ints.UpstashIntegration, error) }