Skip to content

Commit

Permalink
Merge pull request #1019 from jetstreamapp/feat/add-organizations
Browse files Browse the repository at this point in the history
Add Jetstream Organizations
  • Loading branch information
paustint committed Sep 6, 2024
2 parents 5261521 + 2eed46f commit 3b099b7
Show file tree
Hide file tree
Showing 47 changed files with 1,967 additions and 180 deletions.
86 changes: 86 additions & 0 deletions apps/api/src/app/controllers/jetstream-organizations.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { z } from 'zod';
import * as jetstreamOrganizationsDb from '../db/organization.db';
import { UserFacingError } from '../utils/error-handler';
import { sendJson } from '../utils/response.handlers';
import { createRoute } from '../utils/route.utils';

export const routeDefinition = {
getOrganizations: {
controllerFn: () => getOrganizations,
validators: {
hasSourceOrg: false,
},
},
createOrganization: {
controllerFn: () => createOrganization,
validators: {
body: z.object({
name: z.string(),
description: z.string().optional(),
}),
hasSourceOrg: false,
},
},
updateOrganization: {
controllerFn: () => updateOrganization,
validators: {
params: z.object({
id: z.string().uuid(),
}),
body: z.object({
name: z.string(),
description: z.string().optional(),
}),
hasSourceOrg: false,
},
},
deleteOrganization: {
controllerFn: () => deleteOrganization,
validators: {
params: z.object({
id: z.string().uuid(),
}),
hasSourceOrg: false,
},
},
};

const getOrganizations = createRoute(routeDefinition.getOrganizations.validators, async ({ user, query }, req, res, next) => {
try {
const organizations = await jetstreamOrganizationsDb.findByUserId({ userId: user.id });

sendJson(res, organizations);
} catch (ex) {
next(new UserFacingError(ex));
}
});

const createOrganization = createRoute(routeDefinition.createOrganization.validators, async ({ user, body }, req, res, next) => {
try {
const organization = await jetstreamOrganizationsDb.create(user.id, body);

sendJson(res, organization);
} catch (ex) {
next(new UserFacingError(ex));
}
});

const updateOrganization = createRoute(routeDefinition.updateOrganization.validators, async ({ body, params, user }, req, res, next) => {
try {
const organization = await jetstreamOrganizationsDb.update(user.id, params.id, body);

sendJson(res, organization, 201);
} catch (ex) {
next(new UserFacingError(ex));
}
});

const deleteOrganization = createRoute(routeDefinition.deleteOrganization.validators, async ({ params, user }, req, res, next) => {
try {
await jetstreamOrganizationsDb.deleteOrganization(user.id, params.id);

sendJson(res, undefined, 204);
} catch (ex) {
next(new UserFacingError(ex));
}
});
34 changes: 29 additions & 5 deletions apps/api/src/app/controllers/oauth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ENV, getExceptionLog, logger } from '@jetstream/api-config';
import { ApiConnection, ApiRequestError, getApiRequestFactoryFn } from '@jetstream/salesforce-api';
import { ERROR_MESSAGES } from '@jetstream/shared/constants';
import { SObjectOrganization, SalesforceOrgUi } from '@jetstream/types';
import { getErrorMessage } from '@jetstream/shared/utils';
import { Maybe, SObjectOrganization, SalesforceOrgUi } from '@jetstream/types';
import { CallbackParamsType } from 'openid-client';
import { z } from 'zod';
import * as jetstreamOrganizationsDb from '../db/organization.db';
import * as salesforceOrgsDb from '../db/salesforce-org.db';
import * as oauthService from '../services/oauth.service';
import { createRoute } from '../utils/route.utils';
Expand All @@ -19,6 +21,7 @@ export const routeDefinition = {
.enum(['true', 'false'])
.nullish()
.transform((val) => val === 'true'),
jetstreamOrganizationId: z.string().nullish(),
}),
hasSourceOrg: false,
},
Expand All @@ -38,9 +41,9 @@ export const routeDefinition = {
* @param res
*/
const salesforceOauthInitAuth = createRoute(routeDefinition.salesforceOauthInitAuth.validators, async ({ query }, req, res, next) => {
const { loginUrl, addLoginParam } = query;
const { loginUrl, addLoginParam, jetstreamOrganizationId } = query;
const { authorizationUrl, code_verifier, nonce, state } = oauthService.salesforceOauthInit(loginUrl, { addLoginParam });
req.session.orgAuth = { code_verifier, nonce, state, loginUrl };
req.session.orgAuth = { code_verifier, nonce, state, loginUrl, jetstreamOrganizationId };
res.redirect(authorizationUrl);
});

Expand Down Expand Up @@ -78,7 +81,7 @@ const salesforceOauthCallback = createRoute(routeDefinition.salesforceOauthCallb
return res.redirect(`/oauth-link/?${new URLSearchParams(returnParams as any).toString().replaceAll('+', '%20')}`);
}

const { code_verifier, nonce, state, loginUrl } = orgAuth;
const { code_verifier, nonce, state, loginUrl, jetstreamOrganizationId } = orgAuth;

const { access_token, refresh_token, userInfo } = await oauthService.salesforceOauthCallback(loginUrl, query, {
code_verifier,
Expand All @@ -101,6 +104,7 @@ const salesforceOauthCallback = createRoute(routeDefinition.salesforceOauthCallb
const salesforceOrg = await initConnectionFromOAuthResponse({
jetstreamConn,
userId: user.id,
jetstreamOrganizationId,
});

returnParams.data = JSON.stringify(salesforceOrg);
Expand All @@ -115,7 +119,15 @@ const salesforceOauthCallback = createRoute(routeDefinition.salesforceOauthCallb
}
});

export async function initConnectionFromOAuthResponse({ jetstreamConn, userId }: { jetstreamConn: ApiConnection; userId: string }) {
export async function initConnectionFromOAuthResponse({
jetstreamConn,
userId,
jetstreamOrganizationId,
}: {
jetstreamConn: ApiConnection;
userId: string;
jetstreamOrganizationId?: Maybe<string>;
}) {
const identity = await jetstreamConn.org.identity();
let companyInfoRecord: SObjectOrganization | undefined;

Expand Down Expand Up @@ -157,6 +169,18 @@ export async function initConnectionFromOAuthResponse({ jetstreamConn, userId }:
orgTrialExpirationDate: companyInfoRecord?.TrialExpirationDate,
};

if (jetstreamOrganizationId) {
try {
salesforceOrgUi.jetstreamOrganizationId = (await jetstreamOrganizationsDb.findById({ id: jetstreamOrganizationId, userId })).id;
} catch (ex) {
logger.warn(
{ userId, jetstreamOrganizationId, ...getExceptionLog(ex) },
'Error getting jetstream org with provided id %s',
getErrorMessage(ex)
);
}
}

const salesforceOrg = await salesforceOrgsDb.createOrUpdateSalesforceOrg(userId, salesforceOrgUi);
return salesforceOrg;
}
23 changes: 23 additions & 0 deletions apps/api/src/app/controllers/orgs.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ export const routeDefinition = {
controllerFn: () => checkOrgHealth,
validators: {},
},
moveOrg: {
controllerFn: () => moveOrg,
validators: {
params: z.object({
uniqueId: z.string().min(1),
}),
body: z.object({
jetstreamOrganizationId: z.string().uuid().nullish(),
}),
hasSourceOrg: false,
},
},
};

const getOrgs = createRoute(routeDefinition.getOrgs.validators, async ({ user }, req, res, next) => {
Expand Down Expand Up @@ -121,3 +133,14 @@ const checkOrgHealth = createRoute(routeDefinition.checkOrgHealth.validators, as
next(new UserFacingError(ex));
}
});

const moveOrg = createRoute(routeDefinition.moveOrg.validators, async ({ body, params, user }, req, res, next) => {
try {
const { uniqueId } = params;
const salesforceOrg = await salesforceOrgsDb.moveSalesforceOrg(user.id, uniqueId, body);

sendJson(res, salesforceOrg, 201);
} catch (ex) {
next(new UserFacingError(ex));
}
});
67 changes: 67 additions & 0 deletions apps/api/src/app/db/organization.db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { prisma } from '@jetstream/api-config';
import { Maybe } from '@jetstream/types';
import { Prisma } from '@prisma/client';
import { findIdByUserId } from './user.db';

const SELECT = Prisma.validator<Prisma.JetstreamOrganizationSelect>()({
id: true,
orgs: {
select: { uniqueId: true },
},
name: true,
description: true,
createdAt: true,
updatedAt: true,
});

export const findByUserId = async ({ userId }: { userId: string }) => {
return await prisma.jetstreamOrganization.findMany({ where: { user: { userId } }, select: SELECT });
};

export const findById = async ({ id, userId }: { id: string; userId: string }) => {
return await prisma.jetstreamOrganization.findFirstOrThrow({ where: { id, user: { userId } }, select: SELECT });
};

export const create = async (
userId: string,
payload: {
name: string;
description?: Maybe<string>;
}
) => {
const userActualId = await findIdByUserId({ userId });
return await prisma.jetstreamOrganization.create({
select: SELECT,
data: {
userId: userActualId,
name: payload.name.trim(),
description: payload.description?.trim(),
},
});
};

export const update = async (
userId,
id,
payload: {
name: string;
description?: Maybe<string>;
}
) => {
return await prisma.jetstreamOrganization.update({
select: SELECT,
where: { user: { userId }, id },
data: {
name: payload.name.trim(),
description: payload.description?.trim() ?? null,
},
});
};

export const deleteOrganization = async (userId, id) => {
return await prisma.jetstreamOrganization.delete({
select: SELECT,
where: { user: { userId }, id },
});
};
28 changes: 27 additions & 1 deletion apps/api/src/app/db/salesforce-org.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { parseISO } from 'date-fns/parseISO';
import isUndefined from 'lodash/isUndefined';

const SELECT = Prisma.validator<Prisma.SalesforceOrgSelect>()({
jetstreamOrganizationId: true,
uniqueId: true,
label: true,
filterText: true,
Expand All @@ -33,6 +34,8 @@ const SELECT = Prisma.validator<Prisma.SalesforceOrgSelect>()({
updatedAt: true,
});

export const SALESFORCE_ORG_SELECT = SELECT;

/**
* TODO: add better error handling with non-db error messages!
*/
Expand Down Expand Up @@ -111,6 +114,8 @@ export async function createOrUpdateSalesforceOrg(jetstreamUserId: string, sales
where: findUniqueOrg({ jetstreamUserId, uniqueId: salesforceOrgUi.uniqueId! }),
});

// FIXME: need to include organization - added orgs should be added to current organization

let orgToDelete: Maybe<{ id: number }>;
/**
* After a sandbox refresh, the orgId will change but the username will remain the same
Expand All @@ -130,7 +135,8 @@ export async function createOrUpdateSalesforceOrg(jetstreamUserId: string, sales
}

if (existingOrg) {
const data: Prisma.SalesforceOrgUpdateInput = {
const data: Prisma.XOR<Prisma.SalesforceOrgUpdateInput, Prisma.SalesforceOrgUncheckedUpdateInput> = {
jetstreamOrganizationId: salesforceOrgUi.jetstreamOrganizationId ?? existingOrg.jetstreamOrganizationId,
uniqueId: salesforceOrgUi.uniqueId ?? existingOrg.uniqueId,
accessToken: salesforceOrgUi.accessToken ?? existingOrg.accessToken,
instanceUrl: salesforceOrgUi.instanceUrl ?? existingOrg.instanceUrl,
Expand Down Expand Up @@ -174,6 +180,7 @@ export async function createOrUpdateSalesforceOrg(jetstreamUserId: string, sales
data: {
jetstreamUserId,
jetstreamUrl: ENV.JETSTREAM_SERVER_URL,
jetstreamOrganizationId: salesforceOrgUi.jetstreamOrganizationId,
label: salesforceOrgUi.label || salesforceOrgUi.username,
uniqueId: salesforceOrgUi.uniqueId!,
accessToken: salesforceOrgUi.accessToken!,
Expand Down Expand Up @@ -229,6 +236,25 @@ export async function updateSalesforceOrg(jetstreamUserId: string, uniqueId: str
});
}

export async function moveSalesforceOrg(jetstreamUserId: string, uniqueId: string, data: { jetstreamOrganizationId?: Maybe<string> }) {
const existingOrg = await prisma.salesforceOrg.findUnique({
select: { id: true },
where: findUniqueOrg({ jetstreamUserId, uniqueId }),
});

if (!existingOrg) {
throw new Error('An org does not exist with the provided input');
}

return await prisma.salesforceOrg.update({
select: SELECT,
where: { id: existingOrg.id },
data: {
jetstreamOrganizationId: data.jetstreamOrganizationId ?? null,
},
});
}

export async function deleteSalesforceOrg(jetstreamUserId: string, uniqueId: string) {
const existingOrg = await prisma.salesforceOrg.findUnique({
select: { id: true, username: true, label: true, orgName: true },
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/app/db/user.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const userSelect: Prisma.UserSelect = {
userId: true,
};

export const findIdByUserId = ({ userId }: { userId: string }) => {
return prisma.user.findFirstOrThrow({ where: { userId }, select: { id: true } }).then(({ id }) => id);
};

/**
* Find by Auth0 userId, not Jetstream Id
*/
Expand Down Expand Up @@ -72,6 +76,7 @@ export async function createOrUpdateUser(user: UserProfileServer): Promise<{ cre
where: { userId: user.id },
data: {
appMetadata: JSON.stringify(user._json[ENV.AUTH_AUDIENCE!]),
deletedAt: null,
preferences: {
upsert: {
create: { skipFrontdoorLogin: false },
Expand All @@ -92,6 +97,7 @@ export async function createOrUpdateUser(user: UserProfileServer): Promise<{ cre
nickname: user._json.nickname,
picture: user._json.picture,
appMetadata: JSON.stringify(user._json[ENV.AUTH_AUDIENCE!]),
deletedAt: null,
preferences: { create: { skipFrontdoorLogin: false } },
},
select: userSelect,
Expand Down
7 changes: 7 additions & 0 deletions apps/api/src/app/routes/api.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import express from 'express';
import Router from 'express-promise-router';
import multer from 'multer';
import { routeDefinition as imageController } from '../controllers/image.controller';
import { routeDefinition as jetstreamOrganizationsController } from '../controllers/jetstream-organizations.controller';
import { routeDefinition as orgsController } from '../controllers/orgs.controller';
import { routeDefinition as salesforceApiReqController } from '../controllers/salesforce-api-requests.controller';
import { routeDefinition as bulkApiController } from '../controllers/sf-bulk-api.controller';
Expand Down Expand Up @@ -50,6 +51,12 @@ routes.post('/orgs/health-check', orgsController.checkOrgHealth.controllerFn());
routes.get('/orgs', orgsController.getOrgs.controllerFn());
routes.patch('/orgs/:uniqueId', orgsController.updateOrg.controllerFn());
routes.delete('/orgs/:uniqueId', orgsController.deleteOrg.controllerFn());
routes.put('/orgs/:uniqueId/move', orgsController.moveOrg.controllerFn());

routes.get('/jetstream-organizations', jetstreamOrganizationsController.getOrganizations.controllerFn());
routes.post('/jetstream-organizations', jetstreamOrganizationsController.createOrganization.controllerFn());
routes.put('/jetstream-organizations/:id', jetstreamOrganizationsController.updateOrganization.controllerFn());
routes.delete('/jetstream-organizations/:id', jetstreamOrganizationsController.deleteOrganization.controllerFn());

/**
* ************************************
Expand Down
Loading

0 comments on commit 3b099b7

Please sign in to comment.