Skip to content

Commit ec58e59

Browse files
fix: Add new route to create team routing-form response again(after revert earlier) (calcom#22407)
* Revert "revert: "fix: Add new route to create team routing-form response (calcom#22347)" (calcom#22399)" This reverts commit e6c36af. * Update the documentation * Remove versioning
1 parent 4f95cff commit ec58e59

15 files changed

Lines changed: 5458 additions & 1452 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Or } from "./or.guard";
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { ExecutionContext, CanActivate } from "@nestjs/common";
2+
3+
import { Or } from "./or.guard";
4+
5+
// Mock guards for testing
6+
class MockGuard1 implements CanActivate {
7+
constructor(private shouldPass: boolean) {}
8+
9+
async canActivate(): Promise<boolean> {
10+
return this.shouldPass;
11+
}
12+
}
13+
14+
class MockGuard2 implements CanActivate {
15+
constructor(private shouldPass: boolean) {}
16+
17+
async canActivate(): Promise<boolean> {
18+
return this.shouldPass;
19+
}
20+
}
21+
22+
class MockGuard3 implements CanActivate {
23+
constructor(private shouldThrow: boolean = false) {}
24+
25+
async canActivate(): Promise<boolean> {
26+
if (this.shouldThrow) {
27+
throw new Error("Guard failed");
28+
}
29+
return false;
30+
}
31+
}
32+
33+
describe("OrGuard", () => {
34+
let guard: InstanceType<ReturnType<typeof Or>>;
35+
let mockExecutionContext: ExecutionContext;
36+
let mockModuleRef: any;
37+
38+
beforeEach(() => {
39+
mockModuleRef = {
40+
get: jest.fn(),
41+
};
42+
43+
const OrGuardClass = Or([MockGuard1, MockGuard2]);
44+
guard = new OrGuardClass(mockModuleRef);
45+
mockExecutionContext = {} as ExecutionContext;
46+
});
47+
48+
it("should be defined", () => {
49+
expect(guard).toBeDefined();
50+
});
51+
52+
it("should grant access when first guard passes", async () => {
53+
const mockGuard1 = new MockGuard1(true);
54+
const mockGuard2 = new MockGuard2(false);
55+
56+
mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);
57+
58+
const result = await guard.canActivate(mockExecutionContext);
59+
60+
expect(result).toBe(true);
61+
});
62+
63+
it("should grant access when second guard passes", async () => {
64+
const mockGuard1 = new MockGuard1(false);
65+
const mockGuard2 = new MockGuard2(true);
66+
67+
mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);
68+
69+
const result = await guard.canActivate(mockExecutionContext);
70+
71+
expect(result).toBe(true);
72+
});
73+
74+
it("should deny access when all guards fail", async () => {
75+
const mockGuard1 = new MockGuard1(false);
76+
const mockGuard2 = new MockGuard2(false);
77+
78+
mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);
79+
80+
const result = await guard.canActivate(mockExecutionContext);
81+
82+
expect(result).toBe(false);
83+
});
84+
85+
it("should continue checking other guards when one throws an error", async () => {
86+
const mockGuard1 = new MockGuard3(true); // throws error
87+
const mockGuard2 = new MockGuard2(true); // passes
88+
89+
mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);
90+
91+
const result = await guard.canActivate(mockExecutionContext);
92+
93+
expect(result).toBe(true);
94+
});
95+
});
96+
97+
describe("Or decorator", () => {
98+
it("should create a guard class with the specified guards", () => {
99+
const OrGuardClass = Or([MockGuard1, MockGuard2]);
100+
101+
expect(OrGuardClass).toBeDefined();
102+
expect(typeof OrGuardClass).toBe("function");
103+
});
104+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Injectable, CanActivate, ExecutionContext, Type, Logger } from "@nestjs/common";
2+
import { ModuleRef } from "@nestjs/core";
3+
4+
/**
5+
* Decorator function that creates an Or guard with the specified guards
6+
* @param guards Array of guard classes to evaluate with OR logic
7+
* @returns A guard class that grants access if ANY of the provided guards return true
8+
*/
9+
export function Or(guards: Type<CanActivate>[]) {
10+
@Injectable()
11+
class OrGuard implements CanActivate {
12+
public readonly logger = new Logger("OrGuard");
13+
14+
constructor(public readonly moduleRef: ModuleRef) {}
15+
16+
async canActivate(context: ExecutionContext): Promise<boolean> {
17+
let lastError: unknown | null = null;
18+
for (const Guard of guards) {
19+
try {
20+
const guardInstance = this.moduleRef.get(Guard, { strict: false });
21+
const result = await Promise.resolve(guardInstance.canActivate(context));
22+
23+
if (result === true) {
24+
this.logger.log(`OrGuard - Guard ${Guard.name} granted access`);
25+
return true; // Access granted if any guard returns true
26+
}
27+
} catch (error) {
28+
lastError = error;
29+
// If a guard throws an exception, it implies failure for that specific guard.
30+
// We catch it and continue checking other guards in the OR chain.
31+
// If an exception should stop the entire chain immediately, re-throw it here.
32+
this.logger.log(
33+
`OrGuard - Guard ${Guard.name} failed: ${
34+
error instanceof Error ? error.message : "Unknown error"
35+
}`
36+
);
37+
}
38+
}
39+
40+
this.logger.log("OrGuard - All guards failed, access denied");
41+
if (lastError) {
42+
throw lastError;
43+
}
44+
return false;
45+
}
46+
}
47+
48+
return OrGuard;
49+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
2+
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
3+
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
4+
import { Request } from "express";
5+
6+
import { Team } from "@calcom/prisma/client";
7+
8+
@Injectable()
9+
export class IsUserRoutingForm implements CanActivate {
10+
constructor(private readonly dbRead: PrismaReadService) {}
11+
12+
async canActivate(context: ExecutionContext): Promise<boolean> {
13+
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
14+
const routingFormId: string = request.params.routingFormId;
15+
const user = request.user as ApiAuthGuardUser;
16+
if (!routingFormId) {
17+
throw new ForbiddenException("IsUserRoutingForm - No routing form id found in request params.");
18+
}
19+
20+
const userRoutingForm = await this.dbRead.prisma.app_RoutingForms_Form.findFirst({
21+
where: {
22+
id: routingFormId,
23+
userId: Number(user.id),
24+
teamId: null,
25+
},
26+
select: {
27+
id: true,
28+
},
29+
});
30+
31+
if (!userRoutingForm) {
32+
throw new ForbiddenException(
33+
`Routing Form with id=${routingFormId} is not a user Routing Form owned by user with id=${user.id}.`
34+
);
35+
}
36+
37+
return true;
38+
}
39+
}

0 commit comments

Comments
 (0)