Skip to content

Commit 0fc2fd6

Browse files
authored
fix: opportunity slack channel to validate from opp id (#3429)
1 parent 377ffc3 commit 0fc2fd6

4 files changed

Lines changed: 41 additions & 82 deletions

File tree

__tests__/schema/opportunity.ts

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import type { ZodError } from 'zod';
22
import { DataSource, IsNull } from 'typeorm';
33
import request from 'supertest';
4-
import { Alerts, Feed, Keyword, User } from '../../src/entity';
4+
import { Alerts, Keyword, User } from '../../src/entity';
55
import { Opportunity } from '../../src/entity/opportunities/Opportunity';
66
import { OpportunityMatch } from '../../src/entity/OpportunityMatch';
77
import { Organization } from '../../src/entity/Organization';
8-
import {
9-
ContentPreferenceOrganization,
10-
ContentPreferenceOrganizationStatus,
11-
} from '../../src/entity/contentPreference/ContentPreferenceOrganization';
8+
129
import { OpportunityKeyword } from '../../src/entity/OpportunityKeyword';
1310
import { DatasetLocation } from '../../src/entity/dataset/DatasetLocation';
1411
import { OpportunityLocation } from '../../src/entity/opportunities/OpportunityLocation';
@@ -5737,12 +5734,12 @@ describe('mutation parseOpportunity', () => {
57375734
describe('mutation createSharedSlackChannel', () => {
57385735
const MUTATION = /* GraphQL */ `
57395736
mutation CreateSharedSlackChannel(
5740-
$organizationId: ID!
5737+
$opportunityId: ID!
57415738
$email: String!
57425739
$channelName: String!
57435740
) {
57445741
createSharedSlackChannel(
5745-
organizationId: $organizationId
5742+
opportunityId: $opportunityId
57465743
email: $email
57475744
channelName: $channelName
57485745
) {
@@ -5755,22 +5752,6 @@ describe('mutation createSharedSlackChannel', () => {
57555752
// Reset all mocks before each test
57565753
mockConversationsCreate.mockReset();
57575754
mockConversationsInviteShared.mockReset();
5758-
5759-
// Add organization membership for user 1
5760-
await con.getRepository(Feed).save({
5761-
id: '1',
5762-
name: 'My Feed',
5763-
userId: '1',
5764-
});
5765-
await saveFixtures(con, ContentPreferenceOrganization, [
5766-
{
5767-
userId: '1',
5768-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5769-
referenceId: '550e8400-e29b-41d4-a716-446655440000',
5770-
status: ContentPreferenceOrganizationStatus.Free,
5771-
feedId: '1',
5772-
},
5773-
]);
57745755
});
57755756

57765757
it('should require authentication', async () => {
@@ -5779,7 +5760,7 @@ describe('mutation createSharedSlackChannel', () => {
57795760
{
57805761
mutation: MUTATION,
57815762
variables: {
5782-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5763+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
57835764
email: 'user@example.com',
57845765
channelName: 'test-channel',
57855766
},
@@ -5796,7 +5777,7 @@ describe('mutation createSharedSlackChannel', () => {
57965777
{
57975778
mutation: MUTATION,
57985779
variables: {
5799-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5780+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
58005781
email: 'user@example.com',
58015782
channelName: 'test-channel',
58025783
},
@@ -5830,7 +5811,7 @@ describe('mutation createSharedSlackChannel', () => {
58305811

58315812
const res = await client.mutate(MUTATION, {
58325813
variables: {
5833-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5814+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
58345815
email: 'user@example.com',
58355816
channelName: 'test-channel',
58365817
},
@@ -5878,7 +5859,7 @@ describe('mutation createSharedSlackChannel', () => {
58785859

58795860
const res = await client.mutate(MUTATION, {
58805861
variables: {
5881-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5862+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
58825863
email: 'user@example.com',
58835864
channelName: 'existing-channel',
58845865
},
@@ -5915,7 +5896,7 @@ describe('mutation createSharedSlackChannel', () => {
59155896

59165897
const res = await client.mutate(MUTATION, {
59175898
variables: {
5918-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5899+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
59195900
email: 'user@example.com',
59205901
channelName: 'test-channel',
59215902
},
@@ -5925,14 +5906,14 @@ describe('mutation createSharedSlackChannel', () => {
59255906
});
59265907

59275908
it('should forbid non-members from creating slack channels', async () => {
5928-
loggedUser = '2'; // User 2 is not a member of organization 550e8400
5909+
loggedUser = '2'; // User 2 is not a recrioter of opporunity/org 550e8400
59295910

59305911
await testMutationErrorCode(
59315912
client,
59325913
{
59335914
mutation: MUTATION,
59345915
variables: {
5935-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5916+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
59365917
email: 'user@example.com',
59375918
channelName: 'test-channel',
59385919
},
@@ -5942,24 +5923,22 @@ describe('mutation createSharedSlackChannel', () => {
59425923
});
59435924

59445925
it('should require active subscription', async () => {
5945-
loggedUser = '2';
5926+
loggedUser = '1';
59465927

5947-
// Create organization membership for user 2
5948-
await con.getRepository(ContentPreferenceOrganization).save({
5949-
userId: '2',
5950-
organizationId: 'ed487a47-6f4d-480f-9712-f48ab29db27c',
5951-
referenceId: 'ed487a47-6f4d-480f-9712-f48ab29db27c',
5952-
status: ContentPreferenceOrganizationStatus.Free,
5953-
feedId: '1',
5928+
// Create a recruiter record for the logged-in user
5929+
await con.getRepository(OpportunityUserRecruiter).save({
5930+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
5931+
userId: '1',
5932+
type: OpportunityUserType.Recruiter,
59545933
});
59555934

59565935
// Update organization to have inactive subscription
59575936
await con.getRepository(Organization).update(
5958-
{ id: 'ed487a47-6f4d-480f-9712-f48ab29db27c' },
5937+
{ id: '550e8400-e29b-41d4-a716-446655440000' },
59595938
{
59605939
recruiterSubscriptionFlags: updateRecruiterSubscriptionFlags({
59615940
subscriptionId: 'sub_456',
5962-
status: SubscriptionStatus.Pending,
5941+
status: SubscriptionStatus.Cancelled,
59635942
provider: 'paddle',
59645943
items: [{ priceId: 'pri_456', quantity: 3 }],
59655944
}),
@@ -5971,7 +5950,7 @@ describe('mutation createSharedSlackChannel', () => {
59715950
{
59725951
mutation: MUTATION,
59735952
variables: {
5974-
organizationId: 'ed487a47-6f4d-480f-9712-f48ab29db27c',
5953+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
59755954
email: 'user@example.com',
59765955
channelName: 'test-channel',
59775956
},
@@ -6009,7 +5988,7 @@ describe('mutation createSharedSlackChannel', () => {
60095988
{
60105989
mutation: MUTATION,
60115990
variables: {
6012-
organizationId: '550e8400-e29b-41d4-a716-446655440000',
5991+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
60135992
email: 'user@example.com',
60145993
channelName: 'new-channel',
60155994
},

src/common/opportunity/accessControl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export enum OpportunityPermissions {
1010
Edit = 'opportunity_edit',
1111
UpdateState = 'opportunity_update_state',
1212
ViewDraft = 'opportunity_view_draft',
13+
CreateSlackChannel = 'opportunity_create_slack_channel',
1314
}
1415

1516
export const ensureOpportunityPermissions = async ({
@@ -43,6 +44,7 @@ export const ensureOpportunityPermissions = async ({
4344
OpportunityPermissions.Edit,
4445
OpportunityPermissions.UpdateState,
4546
OpportunityPermissions.ViewDraft,
47+
OpportunityPermissions.CreateSlackChannel,
4648
].includes(permission)
4749
) {
4850
const opportunityUserQb = con

src/common/schema/opportunities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export const reimportOpportunitySchema = z
322322
);
323323

324324
export const createSharedSlackChannelSchema = z.object({
325-
organizationId: z.string().uuid('Organization ID must be a valid UUID'),
325+
opportunityId: z.uuid('Opportunity ID must be a valid UUID'),
326326
email: z.string().email('Email must be a valid email address'),
327327
channelName: z
328328
.string()

src/schema/opportunity.ts

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ import { QuestionScreening } from '../entity/questions/QuestionScreening';
7070
import { In, Not, JsonContains, EntityManager } from 'typeorm';
7171
import { Organization } from '../entity/Organization';
7272
import { Source, SourceType } from '../entity/Source';
73-
import { ContentPreferenceOrganization } from '../entity/contentPreference/ContentPreferenceOrganization';
7473
import {
7574
OrganizationLinkType,
7675
SocialMediaType,
@@ -904,9 +903,9 @@ export const typeDefs = /* GraphQL */ `
904903
"""
905904
createSharedSlackChannel(
906905
"""
907-
Organization ID
906+
Opportunity ID
908907
"""
909-
organizationId: ID!
908+
opportunityId: ID!
910909
911910
"""
912911
Email address of the user to invite
@@ -2493,47 +2492,26 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
24932492
payload: z.infer<typeof createSharedSlackChannelSchema>,
24942493
ctx: AuthContext,
24952494
): Promise<GQLEmptyResponse> => {
2496-
const { organizationId, channelName, email } =
2495+
const { opportunityId, channelName, email } =
24972496
createSharedSlackChannelSchema.parse(payload);
24982497

2499-
// Check if the user is a recruiter
2500-
const isRecruiter = await ctx.con
2501-
.getRepository(OpportunityUserRecruiter)
2502-
.findOne({
2503-
where: {
2504-
userId: ctx.userId,
2505-
type: OpportunityUserType.Recruiter,
2506-
},
2507-
});
2508-
2509-
if (!isRecruiter) {
2510-
throw new ForbiddenError(
2511-
'Access denied! Only recruiters can create Slack channels',
2512-
);
2513-
}
2498+
await ensureOpportunityPermissions({
2499+
con: ctx.con.manager,
2500+
userId: ctx.userId,
2501+
opportunityId,
2502+
permission: OpportunityPermissions.CreateSlackChannel,
2503+
isTeamMember: ctx.isTeamMember,
2504+
});
25142505

2515-
// Verify user is a member of the organization
2516-
const organizationMembership = await ctx.con
2517-
.getRepository(ContentPreferenceOrganization)
2518-
.findOne({
2519-
where: {
2520-
userId: ctx.userId,
2521-
organizationId,
2522-
},
2506+
const opportunity = await ctx.con
2507+
.getRepository(OpportunityJob)
2508+
.findOneOrFail({
2509+
where: { id: opportunityId },
2510+
relations: { organization: true },
25232511
});
25242512

2525-
if (!organizationMembership) {
2526-
throw new ForbiddenError(
2527-
'Access denied! You are not a member of this organization',
2528-
);
2529-
}
2530-
25312513
// Get the organization and check subscription status
2532-
const organization = await ctx.con
2533-
.getRepository(Organization)
2534-
.findOneOrFail({
2535-
where: { id: organizationId },
2536-
});
2514+
const organization = await opportunity.organization;
25372515

25382516
// Check if organization has an active subscription
25392517
if (
@@ -2570,7 +2548,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
25702548

25712549
// Mark organization as having a Slack connection and store channel name
25722550
await ctx.con.getRepository(Organization).update(
2573-
{ id: organizationId },
2551+
{ id: organization.id },
25742552
{
25752553
recruiterSubscriptionFlags:
25762554
updateRecruiterSubscriptionFlags<Organization>({

0 commit comments

Comments
 (0)