Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic atoms in barebone example platform apps #13006

Merged
merged 27 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2817149
example app
ThyMinimalDev Dec 21, 2023
d06346d
example app
ThyMinimalDev Dec 21, 2023
bf13d09
dev move
ThyMinimalDev Dec 21, 2023
846e885
fix: more entry points
ThyMinimalDev Dec 22, 2023
9aa2f52
fixup! fix: more entry points
ThyMinimalDev Dec 22, 2023
afb519f
refactor: v2 API (#12913)
supalarry Dec 22, 2023
bd223a9
feat: endpoint for deleting oAuth users & oAuth users returned data (…
supalarry Dec 22, 2023
310e88c
Merge branch 'platform' into pro-159-create-platform-examples-apps
ThyMinimalDev Dec 27, 2023
1bcca12
Merge branch 'platform' into pro-159-create-platform-examples-apps
ThyMinimalDev Dec 29, 2023
2de757f
Connect CalProvider and GCal
ThyMinimalDev Dec 29, 2023
969367e
Connect CalProvider and GCal
ThyMinimalDev Dec 29, 2023
5185f66
return response interceptor to handle failed requests
Ryukemeister Jan 2, 2024
884bd5d
handle failed requests using axios intercepter
Ryukemeister Jan 2, 2024
971abc3
cal provider refresh tokens, external gcal
ThyMinimalDev Jan 2, 2024
38bcee9
external gcal
ThyMinimalDev Jan 2, 2024
ba299f3
cal provider refresh and retries
ThyMinimalDev Jan 3, 2024
1f4804e
remove console.log
ThyMinimalDev Jan 3, 2024
2e49804
refactor
ThyMinimalDev Jan 4, 2024
6c987d9
ignore built atoms css
ThyMinimalDev Jan 4, 2024
1ea3fc8
remove change to token repo
ThyMinimalDev Jan 4, 2024
228d0bb
Merge branch 'platform' into pro-159-create-platform-examples-apps
ThyMinimalDev Jan 4, 2024
70fa027
refactor
ThyMinimalDev Jan 4, 2024
7fdb7e9
refactor
ThyMinimalDev Jan 4, 2024
17386cd
downdgrade vite of unrelated packages
ThyMinimalDev Jan 4, 2024
c5ed90c
move gcal endpoints to platform
ThyMinimalDev Jan 4, 2024
0ec9055
gcal service
ThyMinimalDev Jan 5, 2024
5dcb542
refactor: use atoms provider
ThyMinimalDev Jan 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions apps/api/v2/src/app.logger.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ export class AppLoggerMiddleware implements NestMiddleware {
private logger = new Logger("HTTP");

use(request: Request, response: Response, next: NextFunction): void {
const { ip, method, path: url } = request;
const { ip, method, protocol, originalUrl, path: url } = request;
const userAgent = request.get("user-agent") || "";

response.on("close", () => {
const { statusCode } = response;
const contentLength = response.get("content-length");

this.logger.log(`${method} ${url} ${statusCode} ${contentLength} - ${userAgent} ${ip}`);
this.logger.log(`${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`);
});
next();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";

describe("OAuth Gcal App Endpoints", () => {
describe("Platform Gcal Endpoints", () => {
let app: INestApplication;

let oAuthClient: PlatformOAuthClient;
Expand Down Expand Up @@ -44,7 +44,7 @@ describe("OAuth Gcal App Endpoints", () => {
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.createOAuthManagedUser("managed-user-e2e@gmail.com", oAuthClient.id);
user = await userRepositoryFixture.createOAuthManagedUser("gcal-connect@gmail.com", oAuthClient.id);
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
accessTokenSecret = tokens.accessToken;
refreshTokenSecret = tokens.refreshToken;
Expand Down Expand Up @@ -73,75 +73,74 @@ describe("OAuth Gcal App Endpoints", () => {
expect(user).toBeDefined();
});

it(`/GET/apps/gcal/oauth/redirect: it should respond 401 with invalid access token`, async () => {
it(`/GET/platform/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/apps/gcal/oauth/redirect`)
.get(`/api/v2/platform/gcal/oauth/auth-url`)
.set("Authorization", `Bearer invalid_access_token`)
.expect(401);
});

it(`/GET/apps/gcal/oauth/redirect: it should redirect to google oauth with valid access token `, async () => {
it(`/GET/platform/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/apps/gcal/oauth/redirect`)
.get(`/api/v2/platform/gcal/oauth/auth-url`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.set("origin", "http://localhost:5555")
.expect(301);
const redirectUrl = response.get("location");
expect(redirectUrl).toBeDefined();
expect(redirectUrl).toContain("https://accounts.google.com/o/oauth2/v2/auth");
.expect(200);
const data = response.body.data;
expect(data.authUrl).toBeDefined();
});

it(`/GET/apps/gcal/oauth/save: without oauth code`, async () => {
it(`/GET/platform/gcal/oauth/save: without oauth code`, async () => {
await request(app.getHttpServer())
.get(
`/api/v2/apps/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3Dhttp://localhost:5555&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
`/api/v2/platform/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3Dhttp://localhost:5555&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
)
.expect(400);
});

it(`/GET/apps/gcal/oauth/save: without access token`, async () => {
it(`/GET/platform/gcal/oauth/save: without access token`, async () => {
await request(app.getHttpServer())
.get(
`/api/v2/apps/gcal/oauth/save?state=origin%3Dhttp://localhost:5555&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
`/api/v2/platform/gcal/oauth/save?state=origin%3Dhttp://localhost:5555&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
)
.expect(400);
});

it(`/GET/apps/gcal/oauth/save: without origin`, async () => {
it(`/GET/platform/gcal/oauth/save: without origin`, async () => {
await request(app.getHttpServer())
.get(
`/api/v2/apps/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
`/api/v2/platform/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
)
.expect(400);
});

it(`/GET/apps/gcal/oauth/check with access token`, async () => {
it(`/GET/platform/gcal/check with access token`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/apps/gcal/oauth/check`)
.get(`/api/v2/platform/gcal/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.expect(400);
});

it(`/GET/apps/gcal/oauth/check without access token`, async () => {
await request(app.getHttpServer()).get(`/api/v2/apps/gcal/oauth/check`).expect(401);
it(`/GET/platform/gcal/check without access token`, async () => {
await request(app.getHttpServer()).get(`/api/v2/platform/gcal/check`).expect(401);
});

it(`/GET/apps/gcal/oauth/check with access token but no credentials`, async () => {
it(`/GET/platform/gcal/check with access token but no credentials`, async () => {
await request(app.getHttpServer())
.get(`/api/v2/apps/gcal/oauth/check`)
.get(`/api/v2/platform/gcal/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.expect(400);
});

it(`/GET/apps/gcal/oauth/check with access token and gcal credentials`, async () => {
it(`/GET/platform/gcal/check with access token and gcal credentials`, async () => {
gcalCredentials = await credentialsRepositoryFixture.create(
"google_calendar",
{},
user.id,
"google-calendar"
);
await request(app.getHttpServer())
.get(`/api/v2/apps/gcal/oauth/check`)
.get(`/api/v2/platform/gcal/check`)
.set("Authorization", `Bearer ${accessTokenSecret}`)
.expect(200);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppsRepository } from "@/modules/apps/apps.repository";
import { GcalService } from "@/modules/apps/services/gcal.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
Expand All @@ -11,19 +12,19 @@ import {
HttpCode,
HttpStatus,
Logger,
NotFoundException,
Query,
Redirect,
Req,
UnauthorizedException,
UseGuards,
Headers,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Request } from "express";
import { google } from "googleapis";
import { z } from "zod";

import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GOOGLE_CALENDAR_ID, GOOGLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiRedirectResponseType, ApiResponse } from "@calcom/platform-types";

const CALENDAR_SCOPES = [
Expand All @@ -32,44 +33,40 @@ const CALENDAR_SCOPES = [
];

@Controller({
path: "apps/gcal",
path: "platform/gcal",
version: "2",
})
export class GoogleCalendarOAuthController {
private readonly logger = new Logger("Apps: Gcal Controller");
export class GcalController {
private readonly logger = new Logger("Platform Gcal Provider");

constructor(
private readonly appRepository: AppsRepository,
private readonly credentialRepository: CredentialsRepository,
private readonly tokensRepository: TokensRepository,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
private readonly config: ConfigService
private readonly config: ConfigService,
private readonly gcalService: GcalService
) {}

@Get("/oauth/redirect")
@Redirect(undefined, 301)
@UseGuards(AccessTokenGuard)
async redirect(@Req() req: Request): Promise<ApiRedirectResponseType> {
const app = await this.appRepository.getAppBySlug("google-calendar");

if (!app) {
throw new NotFoundException();
}
private redirectUri = `${this.config.get("api.url")}/platform/gcal/oauth/save`;

const { client_id, client_secret } = z
.object({ client_id: z.string(), client_secret: z.string() })
.parse(app.keys);
const redirect_uri = `${this.config.get("api.url")}/apps/gcal/oauth/save`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const accessToken = req.get("Authorization")?.replace("Bearer ", "");
@Get("/oauth/auth-url")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard)
async redirect(
@Headers("Authorization") authorization: string,
@Req() req: Request
): Promise<ApiResponse<{ authUrl: string }>> {
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
const accessToken = authorization.replace("Bearer ", "");
const origin = req.get("origin") ?? req.get("host");
const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline",
scope: CALENDAR_SCOPES,
prompt: "consent",
state: `accessToken=${accessToken}&origin=${origin}`,
});
return { url: authUrl };
return { status: SUCCESS_STATUS, data: { authUrl } };
}

@Get("/oauth/save")
Expand All @@ -79,7 +76,14 @@ export class GoogleCalendarOAuthController {
const stateParams = new URLSearchParams(state);
const { accessToken, origin } = z
.object({ accessToken: z.string(), origin: z.string() })
.parse(stateParams);
.parse({ accessToken: stateParams.get("accessToken"), origin: stateParams.get("origin") });

// User chose not to authorize your app or didn't authorize your app
// redirect directly without oauth code
if (!code) {
return { url: origin };
}

const parsedCode = z.string().parse(code);

const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
Expand All @@ -88,24 +92,13 @@ export class GoogleCalendarOAuthController {
throw new UnauthorizedException("Invalid Access token.");
}

const app = await this.appRepository.getAppBySlug("google-calendar");

if (!app) {
throw new NotFoundException();
}

const { client_id, client_secret } = z
.object({ client_id: z.string(), client_secret: z.string() })
.parse(app.keys);
const redirect_uri = `${this.config.get("api.url")}/apps/gcal/oauth/save`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const oAuth2Client = await this.gcalService.getOAuthClient(this.redirectUri);
const token = await oAuth2Client.getToken(parsedCode);
const key = token.res?.data;
const credential = await this.credentialRepository.createAppCredential(
"google_calendar",
GOOGLE_CALENDAR_TYPE,
key,
ownerId,
"google-calendar"
ownerId
);

oAuth2Client.setCredentials(key);
Expand All @@ -124,14 +117,14 @@ export class GoogleCalendarOAuthController {
primaryCal.id,
credential.id,
ownerId,
"google_calendar"
GOOGLE_CALENDAR_ID
);
}

return { url: origin };
}

@Get("/oauth/check")
@Get("/check")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard)
async check(@GetUser("id") userId: number): Promise<ApiResponse> {
Expand Down
17 changes: 17 additions & 0 deletions apps/api/v2/src/ee/gcal/gcal.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GcalController } from "@/ee/gcal/gcal.controller";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { GcalService } from "@/modules/apps/services/gcal.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

@Module({
imports: [PrismaModule, TokensModule, OAuthClientModule],
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GcalService],
controllers: [GcalController],
})
export class GcalModule {}
14 changes: 14 additions & 0 deletions apps/api/v2/src/ee/platform-endpoints-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { GcalModule } from "@/ee/gcal/gcal.module";
import { ProviderModule } from "@/ee/provider/provider.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";

@Module({
imports: [GcalModule, ProviderModule],
})
export class PlatformEndpointsModule implements NestModule {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
configure(_consumer: MiddlewareConsumer) {
// TODO: apply ratelimits
}
}
68 changes: 68 additions & 0 deletions apps/api/v2/src/ee/provider/provider.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { UserReturned } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import {
BadRequestException,
Controller,
Get,
HttpCode,
HttpStatus,
Logger,
NotFoundException,
Param,
UnauthorizedException,
UseGuards,
} from "@nestjs/common";

import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";

@Controller({
path: "platform/provider",
version: "2",
})
export class CalProviderController {
private readonly logger = new Logger("Platform Provider Controller");

constructor(
private readonly tokensRepository: TokensRepository,
private readonly oauthClientRepository: OAuthClientRepository
) {}

@Get("/:clientId")
@HttpCode(HttpStatus.OK)
async verifyClientId(@Param("clientId") clientId: string): Promise<ApiResponse> {
if (!clientId) {
throw new NotFoundException();
}
const oAuthClient = await this.oauthClientRepository.getOAuthClient(clientId);

if (!oAuthClient) throw new UnauthorizedException();

return {
status: SUCCESS_STATUS,
};
}

@Get("/:clientId/access-token")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard)
async verifyAccessToken(
@Param("clientId") clientId: string,
@GetUser() user: UserReturned
): Promise<ApiResponse> {
if (!clientId) {
throw new BadRequestException();
}

if (!user) {
throw new UnauthorizedException();
}

return {
status: SUCCESS_STATUS,
};
}
}
13 changes: 13 additions & 0 deletions apps/api/v2/src/ee/provider/provider.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CalProviderController } from "@/ee/provider/provider.controller";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { Module } from "@nestjs/common";

@Module({
imports: [PrismaModule, TokensModule, OAuthClientModule],
providers: [CredentialsRepository],
controllers: [CalProviderController],
})
export class ProviderModule {}
Loading
Loading