refactor: v2 API (#12913)

* Use Boolean only instead of git add src/modules/auth/guard/organization-roles/organization-roles.guard.ts

* move tests next to files they test

* replace .. in import paths with absolute path

* camelCase instead of snake_case for access and refresh token variables

* user sanitize function Typescript friendly

* restructure oAuth clients folder: example for other folders

* restructure bookings module

* organize modules in auth, endpoints, repositories, services

* organize auth module

* organize repositories

* organize inputs

* rename OAuthClientGuard to OAuthClientCredentialsGuard

* add error messages

* add error messages

* clientId as param in oauth-flow & schema mapping

* camelCase instead of snake_case for clientId and clientSecret

* access token guard as passport strategy

* folder structure as features

* get rid of index files
This commit is contained in:
Lauris Skraucis 2023-12-22 13:09:25 +02:00 committed by GitHub
parent e18eb6bc4b
commit d4c946a0d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 458 additions and 394 deletions

View File

@ -1,7 +1,7 @@
import { AppLoggerMiddleware } from "@/app.logger.middleware";
import appConfig from "@/config/app";
import { AuthModule } from "@/modules/auth/auth.module";
import { EndpointsModule } from "@/modules/endpoints-module";
import { EndpointsModule } from "@/modules/endpoints.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";

View File

@ -19,7 +19,7 @@ export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environme
if (typeof fallback !== "undefined") {
return fallback;
}
throw new Error(`Missing environment variable: ${key}`);
throw new Error(`Missing environment variable: ${key}.`);
}
return value;

View File

@ -1,16 +1,34 @@
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
import { NextAuthGuard } from "@/modules/auth/guard";
import { NextAuthStrategy } from "@/modules/auth/strategy";
import { ApiKeyAuthStrategy } from "@/modules/auth/strategy/api-key-auth/api-key-auth.strategy";
import { MembershipModule } from "@/modules/membership/membership.module";
import { UserModule } from "@/modules/user/user.module";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
import { AccessTokenStrategy } from "@/modules/auth/strategies/access-token/access-token.strategy";
import { ApiKeyAuthStrategy } from "@/modules/auth/strategies/api-key-auth/api-key-auth.strategy";
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
@Module({
imports: [PassportModule, JwtModule.register({}), ApiKeyModule, UserModule, MembershipModule],
providers: [ApiKeyAuthStrategy, NextAuthGuard, NextAuthStrategy],
exports: [NextAuthGuard],
imports: [
PassportModule,
JwtModule.register({}),
ApiKeyModule,
UsersModule,
MembershipsModule,
TokensModule,
],
providers: [
ApiKeyAuthStrategy,
NextAuthGuard,
NextAuthStrategy,
AccessTokenGuard,
AccessTokenStrategy,
OAuthFlowService,
],
exports: [NextAuthGuard, AccessTokenGuard],
})
export class AuthModule {}

View File

@ -1,2 +0,0 @@
export * from "./get-user/get-user.decorator";
export * from "./roles/roles.decorator";

View File

@ -1,2 +0,0 @@
export * from "./next-auth/next-auth.guard";
export * from "./organization-roles/organization-roles.guard";

View File

@ -1,19 +0,0 @@
import { OAuthFlowService } from "@/modules/oauth/flow/oauth-flow.service";
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
@Injectable()
export class AccessTokenGuard implements CanActivate {
constructor(private readonly oauthFlowService: OAuthFlowService) {}
canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
const bearer = authHeader?.replace("Bearer ", "").trim();
if (!bearer) {
throw new UnauthorizedException();
}
return this.oauthFlowService.validateAccessToken(bearer);
}
}

View File

@ -0,0 +1,7 @@
import { AuthGuard } from "@nestjs/passport";
export class AccessTokenGuard extends AuthGuard("access-token") {
constructor() {
super();
}
}

View File

@ -1,11 +1,11 @@
import { Roles } from "@/modules/auth/decorator/roles/roles.decorator";
import { MembershipRepository } from "@/modules/membership/membership.repository";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
@Injectable()
export class OrganizationRolesGuard implements CanActivate {
constructor(private reflector: Reflector, private membershipRepository: MembershipRepository) {}
constructor(private reflector: Reflector, private membershipRepository: MembershipsRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.get(Roles, context.getHandler());
@ -16,7 +16,7 @@ export class OrganizationRolesGuard implements CanActivate {
const request = context.switchToHttp().getRequest();
const user = request.user;
const partOfOrganization = !!user?.organizationId;
const partOfOrganization = Boolean(user?.organizationId);
if (!user || !partOfOrganization) {
return false;

View File

@ -0,0 +1,54 @@
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import type { Request } from "express";
class BaseStrategy {
success!: (user: unknown) => void;
error!: (error: Error) => void;
}
@Injectable()
export class AccessTokenStrategy extends PassportStrategy(BaseStrategy, "access-token") {
constructor(
private readonly oauthFlowService: OAuthFlowService,
private readonly tokensRepository: TokensRepository,
private readonly userRepository: UsersRepository
) {
super();
}
async authenticate(request: Request) {
try {
const accessToken = request.get("Authorization")?.replace("Bearer ", "");
if (!accessToken) {
throw new UnauthorizedException("Access token is missing or invalid.");
}
const valid = this.oauthFlowService.validateAccessToken(accessToken);
if (!valid) {
throw new UnauthorizedException("Access token is missing or invalid.");
}
const ownerId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
if (!ownerId) {
throw new UnauthorizedException("Invalid access token");
}
const user = await this.userRepository.findById(ownerId);
if (!user) {
throw new UnauthorizedException("User associated with the access token not found.");
}
return this.success(user);
} catch (error) {
if (error instanceof Error) return this.error(error);
}
}
}

View File

@ -1,5 +1,5 @@
import { ApiKeyService } from "@/modules/api-key/api-key.service";
import { UserRepository } from "@/modules/user/user.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import type { Request } from "express";
@ -13,7 +13,7 @@ class BaseStrategy {
export class ApiKeyAuthStrategy extends PassportStrategy(BaseStrategy, "api-key") {
constructor(
private readonly apiKeyService: ApiKeyService,
private readonly userRepository: UserRepository
private readonly userRepository: UsersRepository
) {
super();
}
@ -23,16 +23,16 @@ export class ApiKeyAuthStrategy extends PassportStrategy(BaseStrategy, "api-key"
const apiKey = await this.apiKeyService.retrieveApiKey(req);
if (!apiKey) {
throw new UnauthorizedException();
throw new UnauthorizedException("Authorization token is missing.");
}
if (apiKey.expiresAt && new Date() > apiKey.expiresAt) {
throw new Error("This apiKey is expired");
throw new UnauthorizedException("The API key is expired.");
}
const user = await this.userRepository.findById(apiKey.userId);
if (!user) {
throw new NotFoundException("User not found");
throw new NotFoundException("User not found.");
}
this.success(user);

View File

@ -1,4 +1,4 @@
import { UserRepository } from "@/modules/user/user.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
@ -12,7 +12,7 @@ class BaseStrategy {
@Injectable()
export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
constructor(private readonly userRepository: UserRepository, private readonly config: ConfigService) {
constructor(private readonly userRepository: UsersRepository, private readonly config: ConfigService) {
super();
}
@ -21,13 +21,17 @@ export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth"
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
const payload = await getToken({ req, secret: nextAuthSecret });
if (!payload || !payload.email) {
throw new UnauthorizedException();
if (!payload) {
throw new UnauthorizedException("Authentication token is missing or invalid.");
}
if (!payload.email) {
throw new UnauthorizedException("Email not found in the authentication token.");
}
const user = await this.userRepository.findByEmail(payload.email);
if (!user) {
throw new UnauthorizedException();
throw new UnauthorizedException("User associated with the authentication token email not found.");
}
return this.success(user);

View File

@ -1,2 +0,0 @@
export * from "./next-auth/next-auth.strategy";
export * from "./api-key-auth/api-key-auth.strategy";

View File

@ -1,11 +0,0 @@
import { BookingController } from "@/modules/booking/booking.controller";
import { BookingRepository } from "@/modules/booking/booking.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [BookingRepository],
controllers: [BookingController],
})
export class BookingModule {}

View File

@ -0,0 +1,11 @@
import { BookingRepository } from "@/modules/bookings/booking.repository";
import { BookingsController } from "@/modules/bookings/controllers/bookings/bookings.controller";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [BookingRepository],
controllers: [BookingsController],
})
export class BookingModule {}

View File

@ -1,4 +1,4 @@
import { CreateBookingInput } from "@/modules/booking/input/create-booking";
import { CreateBookingInput } from "@/modules/bookings/inputs/create-booking.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";

View File

@ -1,6 +1,6 @@
import { GetUser } from "@/modules/auth/decorator";
import { BookingRepository } from "@/modules/booking/booking.repository";
import { CreateBookingInput } from "@/modules/booking/input/create-booking";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { BookingRepository } from "@/modules/bookings/booking.repository";
import { CreateBookingInput } from "@/modules/bookings/inputs/create-booking.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import {
@ -21,7 +21,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
@Controller("booking")
export class BookingController {
export class BookingsController {
private readonly logger = new Logger("BookingController");
constructor(

View File

@ -1,11 +1,10 @@
import { BookingModule } from "@/modules/booking/booking.module";
import { OAuthFlowModule } from "@/modules/oauth/flow/oauth-flow.module";
import { OAuthClientModule } from "@/modules/oauth/oauth-client.module";
import { BookingModule } from "@/modules/bookings/booking.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";
@Module({
imports: [BookingModule, OAuthClientModule, OAuthFlowModule],
imports: [BookingModule, OAuthClientModule],
})
export class EndpointsModule implements NestModule {
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -1,10 +0,0 @@
import { MembershipRepository } from "@/modules/membership/membership.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [MembershipRepository],
exports: [MembershipRepository],
})
export class MembershipModule {}

View File

@ -0,0 +1,10 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [MembershipsRepository],
exports: [MembershipsRepository],
})
export class MembershipsModule {}

View File

@ -2,7 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class MembershipRepository {
export class MembershipsRepository {
constructor(private readonly dbRead: PrismaReadService) {}
async findOrgUserMembership(organizationId: number, userId: number) {

View File

@ -2,10 +2,10 @@ import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { CreateUserResponse } from "@/modules/oauth/user/oauth-user.controller";
import { CreateUserInput } from "@/modules/user/input/create-user";
import { UpdateUserInput } from "@/modules/user/input/update-user";
import { UserModule } from "@/modules/user/user.module";
import { CreateUserResponse } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { UpdateUserInput } from "@/modules/users/inputs/update-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
@ -18,14 +18,14 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiSuccessResponse } from "@calcom/platform-types";
describe("User Endpoints", () => {
describe("OAuth Client Users Endpoints", () => {
describe("Not authenticated", () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UserModule],
imports: [AppModule, UsersModule],
}).compile();
app = moduleRef.createNestApplication();
@ -70,7 +70,7 @@ describe("User Endpoints", () => {
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UserModule],
imports: [AppModule, UsersModule],
}).compile();
app = moduleRef.createNestApplication();
@ -89,7 +89,7 @@ describe("User Endpoints", () => {
const data = {
logo: "logo-url",
name: "name",
redirect_uris: ["redirect-uri"],
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";

View File

@ -1,9 +1,10 @@
import { AccessTokenGuard } from "@/modules/auth/guard/oauth/access-token.guard";
import { OAuthClientGuard } from "@/modules/oauth/guard/oauth-client/oauth-client.guard";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { CreateUserInput } from "@/modules/user/input/create-user";
import { UpdateUserInput } from "@/modules/user/input/update-user";
import { UserRepository } from "@/modules/user/user.repository";
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { UpdateUserInput } from "@/modules/users/inputs/update-user.input";
import { UsersRepository } from "@/modules/users/users.repository";
import {
Body,
Controller,
@ -20,23 +21,23 @@ import {
} from "@nestjs/common";
import { User } from "@prisma/client";
import { DUPLICATE_RESOURCE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
path: "oauth-clients/:clientId/users",
version: "2",
})
export class OAuthUserController {
export class OAuthClientUsersController {
private readonly logger = new Logger("UserController");
constructor(
private readonly userRepository: UserRepository,
private readonly userRepository: UsersRepository,
private readonly tokensRepository: TokensRepository
) {}
@Post("/")
@UseGuards(OAuthClientGuard)
@UseGuards(OAuthClientCredentialsGuard)
async createUser(
@Param("clientId") oAuthClientId: string,
@Body() body: CreateUserInput
@ -48,21 +49,21 @@ export class OAuthUserController {
const existingUser = await this.userRepository.findByEmail(body.email);
if (existingUser) {
throw new BadRequestException(DUPLICATE_RESOURCE);
throw new BadRequestException("A user with the provided email already exists.");
}
const user = await this.userRepository.create(body, oAuthClientId);
const { access_token, refresh_token } = await this.tokensRepository.createOAuthTokens(
const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens(
oAuthClientId,
user.id!
user.id
);
return {
status: SUCCESS_STATUS,
data: {
user,
accessToken: access_token,
refreshToken: refresh_token,
accessToken: accessToken,
refreshToken: refreshToken,
},
};
}
@ -70,10 +71,17 @@ export class OAuthUserController {
@Get("/:userId")
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard)
async getUserById(@Param("userId") userId: number): Promise<ApiResponse<Partial<User>>> {
async getUserById(
@GetUser("id") accessTokenUserId: number,
@Param("userId") userId: number
): Promise<ApiResponse<Partial<User>>> {
if (accessTokenUserId !== userId) {
throw new BadRequestException("You can only access your own user data.");
}
const user = await this.userRepository.findById(userId);
if (!user) {
throw new NotFoundException("User not found");
throw new NotFoundException(`User with ID ${userId} not found.`);
}
return { status: SUCCESS_STATUS, data: user };
@ -83,9 +91,14 @@ export class OAuthUserController {
@HttpCode(HttpStatus.OK)
@UseGuards(AccessTokenGuard)
async updateUser(
@GetUser("id") accessTokenUserId: number,
@Param("userId") userId: number,
@Body() body: UpdateUserInput
): Promise<ApiResponse<Partial<User>>> {
if (accessTokenUserId !== userId) {
throw new BadRequestException("You can only update your own user data.");
}
this.logger.log(`Updating user with ID ${userId}: ${JSON.stringify(body, null, 2)}`);
const user = await this.userRepository.update(userId, body);

View File

@ -1,37 +1,36 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { AuthModule } from "@/modules/auth/auth.module";
import { NextAuthStrategy } from "@/modules/auth/strategy";
import { UpdateOAuthClientInput } from "@/modules/oauth/input/update-oauth-client";
import { OAuthClientModule } from "@/modules/oauth/oauth-client.module";
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UserModule } from "@/modules/user/user.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { Membership, PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { NextAuthMockStrategy } from "test/mocks/next-auth-mock.strategy";
import { withNextAuth } from "test/utils/withNextAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { CreateOAuthClientInput } from "@calcom/platform-types";
import { ApiSuccessResponse } from "@calcom/platform-types";
import { bootstrap } from "../src/app";
import { MembershipRepositoryFixture } from "./fixtures/repository/membership.repository.fixture";
import { TeamRepositoryFixture } from "./fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "./fixtures/repository/users.repository.fixture";
import { NextAuthMockStrategy } from "./mocks/next-auth-mock.strategy";
import { withNextAuth } from "./utils/withNextAuth";
describe("OAuth Client Endpoints", () => {
describe("OAuth Clients Endpoints", () => {
describe("User Not Authenticated", () => {
let appWithoutAuth: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, OAuthClientModule, UserModule, AuthModule, PrismaModule],
imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule],
}).compile();
appWithoutAuth = moduleRef.createNestApplication();
bootstrap(appWithoutAuth as NestExpressApplication);
@ -73,7 +72,7 @@ describe("OAuth Client Endpoints", () => {
userEmail,
Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, OAuthClientModule, UserModule, AuthModule, PrismaModule],
imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule],
})
).compile();
const strategy = moduleRef.get(NextAuthStrategy);
@ -145,7 +144,7 @@ describe("OAuth Client Endpoints", () => {
describe("User is part of an organization as Admin", () => {
let membership: Membership;
let client: { client_id: string; client_secret: string };
let client: { clientId: string; clientSecret: string };
const oAuthClientName = "test-oauth-client-admin";
beforeAll(async () => {
@ -155,7 +154,7 @@ describe("OAuth Client Endpoints", () => {
it(`/POST`, () => {
const body: CreateOAuthClientInput = {
name: oAuthClientName,
redirect_uris: ["http://test-oauth-client.com"],
redirectUris: ["http://test-oauth-client.com"],
permissions: 32,
};
return request(app.getHttpServer())
@ -163,15 +162,15 @@ describe("OAuth Client Endpoints", () => {
.send(body)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<{ client_id: string; client_secret: string }> =
const responseBody: ApiSuccessResponse<{ clientId: string; clientSecret: string }> =
response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.client_id).toBeDefined();
expect(responseBody.data.client_secret).toBeDefined();
expect(responseBody.data.clientId).toBeDefined();
expect(responseBody.data.clientSecret).toBeDefined();
client = {
client_id: responseBody.data.client_id,
client_secret: responseBody.data.client_secret,
clientId: responseBody.data.clientId,
clientSecret: responseBody.data.clientSecret,
};
});
});
@ -189,7 +188,7 @@ describe("OAuth Client Endpoints", () => {
});
it(`/GET/:id`, () => {
return request(app.getHttpServer())
.get(`/api/v2/oauth-clients/${client.client_id}`)
.get(`/api/v2/oauth-clients/${client.clientId}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<PlatformOAuthClient> = response.body;
@ -202,7 +201,7 @@ describe("OAuth Client Endpoints", () => {
const clientUpdatedName = "test-oauth-client-updated";
const body: UpdateOAuthClientInput = { name: clientUpdatedName };
return request(app.getHttpServer())
.put(`/api/v2/oauth-clients/${client.client_id}`)
.put(`/api/v2/oauth-clients/${client.clientId}`)
.send(body)
.expect(200)
.then((response) => {
@ -213,7 +212,7 @@ describe("OAuth Client Endpoints", () => {
});
});
it(`/DELETE/:id`, () => {
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.client_id}`).expect(200);
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.clientId}`).expect(200);
});
afterAll(async () => {
@ -223,7 +222,7 @@ describe("OAuth Client Endpoints", () => {
describe("User is part of an organization as Owner", () => {
let membership: Membership;
let client: { client_id: string; client_secret: string };
let client: { clientId: string; clientSecret: string };
const oAuthClientName = "test-oauth-client-owner";
const oAuthClientPermissions = 32;
@ -234,7 +233,7 @@ describe("OAuth Client Endpoints", () => {
it(`/POST`, () => {
const body: CreateOAuthClientInput = {
name: oAuthClientName,
redirect_uris: ["http://test-oauth-client.com"],
redirectUris: ["http://test-oauth-client.com"],
permissions: 32,
};
return request(app.getHttpServer())
@ -242,15 +241,15 @@ describe("OAuth Client Endpoints", () => {
.send(body)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<{ client_id: string; client_secret: string }> =
const responseBody: ApiSuccessResponse<{ clientId: string; clientSecret: string }> =
response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.client_id).toBeDefined();
expect(responseBody.data.client_secret).toBeDefined();
expect(responseBody.data.clientId).toBeDefined();
expect(responseBody.data.clientSecret).toBeDefined();
client = {
client_id: responseBody.data.client_id,
client_secret: responseBody.data.client_secret,
clientId: responseBody.data.clientId,
clientSecret: responseBody.data.clientSecret,
};
});
});
@ -270,7 +269,7 @@ describe("OAuth Client Endpoints", () => {
});
it(`/GET/:id`, () => {
return request(app.getHttpServer())
.get(`/api/v2/oauth-clients/${client.client_id}`)
.get(`/api/v2/oauth-clients/${client.clientId}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<PlatformOAuthClient> = response.body;
@ -284,7 +283,7 @@ describe("OAuth Client Endpoints", () => {
const clientUpdatedName = "test-oauth-client-updated";
const body: UpdateOAuthClientInput = { name: clientUpdatedName };
return request(app.getHttpServer())
.put(`/api/v2/oauth-clients/${client.client_id}`)
.put(`/api/v2/oauth-clients/${client.clientId}`)
.send(body)
.expect(200)
.then((response) => {
@ -295,7 +294,7 @@ describe("OAuth Client Endpoints", () => {
});
});
it(`/DELETE/:id`, () => {
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.client_id}`).expect(200);
return request(app.getHttpServer()).delete(`/api/v2/oauth-clients/${client.clientId}`).expect(200);
});
afterAll(async () => {

View File

@ -1,9 +1,9 @@
import { GetUser } from "@/modules/auth/decorator";
import { Roles } from "@/modules/auth/decorator/roles/roles.decorator";
import { NextAuthGuard } from "@/modules/auth/guard";
import { OrganizationRolesGuard } from "@/modules/auth/guard/organization-roles/organization-roles.guard";
import { UpdateOAuthClientInput } from "@/modules/oauth/input/update-oauth-client";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import {
Body,
Controller,
@ -29,7 +29,7 @@ import type { ApiResponse } from "@calcom/platform-types";
version: "2",
})
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
export class OAuthClientController {
export class OAuthClientsController {
private readonly logger = new Logger("OAuthClientController");
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
@ -40,7 +40,7 @@ export class OAuthClientController {
async createOAuthClient(
@GetUser("organizationId") organizationId: number,
@Body() body: CreateOAuthClientInput
): Promise<ApiResponse<{ client_id: string; client_secret: string }>> {
): Promise<ApiResponse<{ clientId: string; clientSecret: string }>> {
this.logger.log(
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
);
@ -48,8 +48,8 @@ export class OAuthClientController {
return {
status: SUCCESS_STATUS,
data: {
client_id: id,
client_secret: secret,
clientId: id,
clientSecret: secret,
},
};
}
@ -70,7 +70,7 @@ export class OAuthClientController {
async getOAuthClientById(@Param("clientId") clientId: string): Promise<ApiResponse<PlatformOAuthClient>> {
const client = await this.oauthClientRepository.getOAuthClient(clientId);
if (!client) {
throw new NotFoundException();
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
}
return { status: SUCCESS_STATUS, data: client };
}

View File

@ -0,0 +1,107 @@
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
import { OAuthAuthorizeInput } from "@/modules/oauth-clients/inputs/authorize.input";
import { ExchangeAuthorizationCodeInput } from "@/modules/oauth-clients/inputs/exchange-code.input";
import { RefreshTokenInput } from "@/modules/oauth-clients/inputs/refresh-token.input";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import {
BadRequestException,
Body,
Controller,
Headers,
HttpCode,
HttpStatus,
Param,
Post,
Response,
UseGuards,
} from "@nestjs/common";
import { Response as ExpressResponse } from "express";
import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
path: "oauth/:clientId",
version: "2",
})
export class OAuthFlowController {
constructor(
private readonly oauthClientRepository: OAuthClientRepository,
private readonly tokensRepository: TokensRepository,
private readonly oAuthFlowService: OAuthFlowService
) {}
@Post("/authorize")
@HttpCode(HttpStatus.OK)
@UseGuards(NextAuthGuard)
async authorize(
@Param("clientId") clientId: string,
@Body() body: OAuthAuthorizeInput,
@GetUser("id") userId: number,
@Response() res: ExpressResponse
): Promise<void> {
const oauthClient = await this.oauthClientRepository.getOAuthClient(clientId);
if (!oauthClient) {
throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);
}
if (!oauthClient?.redirectUris.includes(body.redirectUri)) {
throw new BadRequestException("Invalid 'redirect_uri' value.");
}
const { id } = await this.tokensRepository.createAuthorizationToken(clientId, userId);
return res.redirect(`${body.redirectUri}?code=${id}`);
}
@Post("/exchange")
@HttpCode(HttpStatus.OK)
async exchange(
@Headers("Authorization") authorization: string,
@Param("clientId") clientId: string,
@Body() body: ExchangeAuthorizationCodeInput
): Promise<ApiResponse<{ accessToken: string; refreshToken: string }>> {
const bearerToken = authorization.replace("Bearer ", "").trim();
if (!bearerToken) {
throw new BadRequestException("Missing 'Bearer' Authorization header.");
}
const { accessToken: accessToken, refreshToken: refreshToken } =
await this.oAuthFlowService.exchangeAuthorizationToken(bearerToken, clientId, body.clientSecret);
return {
status: SUCCESS_STATUS,
data: {
accessToken,
refreshToken,
},
};
}
@Post("/refresh")
@HttpCode(HttpStatus.OK)
@UseGuards(OAuthClientCredentialsGuard)
async refreshAccessToken(
@Param("clientId") clientId: string,
@Headers(X_CAL_SECRET_KEY) secretKey: string,
@Body() body: RefreshTokenInput
): Promise<ApiResponse<{ accessToken: string; refreshToken: string }>> {
const { accessToken, refreshToken } = await this.oAuthFlowService.refreshToken(
clientId,
secretKey,
body.refreshToken
);
return {
status: SUCCESS_STATUS,
data: {
accessToken: accessToken,
refreshToken: refreshToken,
},
};
}
}

View File

@ -1,5 +1,5 @@
import { AppModule } from "@/app.module";
import { OAuthClientModule } from "@/modules/oauth/oauth-client.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { createMock } from "@golevelup/ts-jest";
import { ExecutionContext, UnauthorizedException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
@ -9,10 +9,10 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { OAuthClientGuard } from "./oauth-client.guard";
import { OAuthClientCredentialsGuard } from "./oauth-client-credentials.guard";
describe("OAuthClientGuard", () => {
let guard: OAuthClientGuard;
describe("OAuthClientCredentialsGuard", () => {
let guard: OAuthClientCredentialsGuard;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let oauthClient: PlatformOAuthClient;
@ -23,7 +23,7 @@ describe("OAuthClientGuard", () => {
imports: [AppModule, OAuthClientModule],
}).compile();
guard = module.get<OAuthClientGuard>(OAuthClientGuard);
guard = module.get<OAuthClientCredentialsGuard>(OAuthClientCredentialsGuard);
teamRepositoryFixture = new TeamRepositoryFixture(module);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
@ -32,7 +32,7 @@ describe("OAuthClientGuard", () => {
const data = {
logo: "logo-url",
name: "name",
redirect_uris: ["redirect-uri"],
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";

View File

@ -1,10 +1,10 @@
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { X_CAL_SECRET_KEY } from "@calcom/platform-constants";
@Injectable()
export class OAuthClientGuard implements CanActivate {
export class OAuthClientCredentialsGuard implements CanActivate {
constructor(private readonly oauthRepository: OAuthClientRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
@ -25,7 +25,7 @@ export class OAuthClientGuard implements CanActivate {
const client = await this.oauthRepository.getOAuthClient(oauthClientId);
if (!client || client.secret !== oauthClientSecret) {
throw new UnauthorizedException();
throw new UnauthorizedException("Invalid client credentials");
}
return true;

View File

@ -2,8 +2,5 @@ import { IsString } from "class-validator";
export class OAuthAuthorizeInput {
@IsString()
redirect_uri!: string;
@IsString()
client_id!: string;
redirectUri!: string;
}

View File

@ -2,8 +2,5 @@ import { IsString } from "class-validator";
export class ExchangeAuthorizationCodeInput {
@IsString()
client_id!: string;
@IsString()
client_secret!: string;
clientSecret!: string;
}

View File

@ -2,5 +2,5 @@ import { IsString } from "class-validator";
export class RefreshTokenInput {
@IsString()
refresh_token!: string;
refreshToken!: string;
}

View File

@ -12,5 +12,5 @@ export class UpdateOAuthClientInput {
@IsArray()
@IsOptional()
@IsString({ each: true })
redirect_uris?: string[] = [];
redirectUris?: string[] = [];
}

View File

@ -0,0 +1,29 @@
import { getEnv } from "@/env";
import { AuthModule } from "@/modules/auth/auth.module";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller";
import { OAuthFlowController } from "@/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller";
import { OAuthClientCredentialsGuard } from "@/modules/oauth-clients/guards/oauth-client-credentials/oauth-client-credentials.guard";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
import { Global, Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
@Global()
@Module({
imports: [
PrismaModule,
AuthModule,
UsersModule,
MembershipsModule,
JwtModule.register({ secret: getEnv("JWT_SECRET") }),
],
providers: [OAuthClientRepository, OAuthClientCredentialsGuard, TokensRepository, OAuthFlowService],
controllers: [OAuthClientUsersController, OAuthClientsController, OAuthFlowController],
exports: [OAuthClientRepository, OAuthClientCredentialsGuard],
})
export class OAuthClientModule {}

View File

@ -1,5 +1,4 @@
import { ExchangeAuthorizationCodeInput } from "@/modules/oauth/flow/input/exchange-code.input";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
@ -30,7 +29,7 @@ export class OAuthFlowService {
const tokenExpiresAt = await this.tokensRepository.getAccessTokenExpiryDate(secret);
if (!tokenExpiresAt) {
throw new UnauthorizedException();
throw new UnauthorizedException("Access token is invalid or not found");
}
if (new Date() > tokenExpiresAt) {
@ -46,12 +45,13 @@ export class OAuthFlowService {
async exchangeAuthorizationToken(
tokenId: string,
input: ExchangeAuthorizationCodeInput
): Promise<{ access_token: string; refresh_token: string }> {
clientId: string,
clientSecret: string
): Promise<{ accessToken: string; refreshToken: string }> {
const oauthClient = await this.oAuthClientRepository.getOAuthClientWithAuthTokens(
tokenId,
input.client_id,
input.client_secret
clientId,
clientSecret
);
if (!oauthClient) {
@ -64,15 +64,15 @@ export class OAuthFlowService {
throw new BadRequestException("Invalid Authorization Token.");
}
const { access_token, refresh_token } = await this.tokensRepository.createOAuthTokens(
input.client_id,
const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens(
clientId,
authorizationToken.owner.id
);
void this.propagateAccessToken(access_token); // voided as we don't need to await
void this.propagateAccessToken(accessToken); // voided as we don't need to await
return {
access_token,
refresh_token,
accessToken,
refreshToken,
};
}
@ -100,8 +100,8 @@ export class OAuthFlowService {
);
return {
access_token: accessToken.secret,
refresh_token: refreshToken.secret,
accessToken: accessToken.secret,
refreshToken: refreshToken.secret,
};
}
}

View File

@ -1,106 +0,0 @@
import { GetUser } from "@/modules/auth/decorator";
import { NextAuthGuard } from "@/modules/auth/guard";
import { OAuthAuthorizeInput } from "@/modules/oauth/flow/input/authorize.input";
import { ExchangeAuthorizationCodeInput } from "@/modules/oauth/flow/input/exchange-code.input";
import { RefreshTokenInput } from "@/modules/oauth/flow/input/refresh-token.input";
import { OAuthFlowService } from "@/modules/oauth/flow/oauth-flow.service";
import { OAuthClientGuard } from "@/modules/oauth/guard/oauth-client/oauth-client.guard";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import {
BadRequestException,
Body,
Controller,
Headers,
HttpCode,
HttpStatus,
Post,
Response,
UseGuards,
} from "@nestjs/common";
import { Response as ExpressResponse } from "express";
import { SUCCESS_STATUS, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
path: "oauth",
version: "2",
})
export class OAuthFlowController {
constructor(
private readonly oauthClientRepository: OAuthClientRepository,
private readonly tokensRepository: TokensRepository,
private readonly oAuthFlowService: OAuthFlowService
) {}
@Post("/authorize")
@HttpCode(HttpStatus.OK)
@UseGuards(NextAuthGuard)
async authorize(
@Body() body: OAuthAuthorizeInput,
@GetUser("id") userId: number,
@Response() res: ExpressResponse
): Promise<void> {
const oauthClient = await this.oauthClientRepository.getOAuthClient(body.client_id);
if (!oauthClient) {
throw new BadRequestException();
}
if (!oauthClient?.redirect_uris.includes(body.redirect_uri)) {
throw new BadRequestException("Invalid 'redirect_uri' value.");
}
const { id } = await this.tokensRepository.createAuthorizationToken(body.client_id, userId);
return res.redirect(`${body.redirect_uri}?code=${id}`);
}
@Post("/exchange")
@HttpCode(HttpStatus.OK)
async exchange(
@Headers("Authorization") authorization: string,
@Body() body: ExchangeAuthorizationCodeInput
): Promise<ApiResponse<{ access_token: string; refresh_token: string }>> {
const bearerToken = authorization.replace("Bearer ", "").trim();
if (!bearerToken) {
throw new BadRequestException("Missing 'Bearer' Authorization header.");
}
const { access_token, refresh_token } = await this.oAuthFlowService.exchangeAuthorizationToken(
bearerToken,
body
);
return {
status: SUCCESS_STATUS,
data: {
access_token,
refresh_token,
},
};
}
@Post("/refresh")
@HttpCode(HttpStatus.OK)
@UseGuards(OAuthClientGuard)
async refreshAccessToken(
@Headers(X_CAL_CLIENT_ID) clientId: string,
@Headers(X_CAL_SECRET_KEY) secretKey: string,
@Body() body: RefreshTokenInput
): Promise<ApiResponse<{ access_token: string; refresh_token: string }>> {
const { access_token, refresh_token } = await this.oAuthFlowService.refreshToken(
clientId,
secretKey,
body.refresh_token
);
return {
status: SUCCESS_STATUS,
data: {
access_token,
refresh_token,
},
};
}
}

View File

@ -1,21 +0,0 @@
import { getEnv } from "@/env";
import { OAuthFlowController } from "@/modules/oauth/flow/oauth-flow.controller";
import { OAuthFlowService } from "@/modules/oauth/flow/oauth-flow.service";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
@Module({
imports: [
JwtModule.register({
secret: getEnv("NEXTAUTH_SECRET"),
}),
PrismaModule,
],
controllers: [OAuthFlowController],
providers: [TokensRepository, OAuthClientRepository, OAuthFlowService],
exports: [OAuthFlowService],
})
export class OAuthFlowModule {}

View File

@ -1,30 +0,0 @@
import { getEnv } from "@/env";
import { AuthModule } from "@/modules/auth/auth.module";
import { MembershipModule } from "@/modules/membership/membership.module";
import { OAuthFlowModule } from "@/modules/oauth/flow/oauth-flow.module";
import { OAuthClientGuard } from "@/modules/oauth/guard/oauth-client/oauth-client.guard";
import { OAuthClientController } from "@/modules/oauth/oauth-client.controller";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import { OAuthUserController } from "@/modules/oauth/user/oauth-user.controller";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UserModule } from "@/modules/user/user.module";
import { Global, Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
@Global()
@Module({
imports: [
PrismaModule,
AuthModule,
UserModule,
MembershipModule,
JwtModule.register({ secret: getEnv("JWT_SECRET") }),
OAuthFlowModule,
TokensModule,
],
providers: [OAuthClientRepository, OAuthClientGuard],
controllers: [OAuthClientController, OAuthUserController],
exports: [OAuthClientRepository, OAuthClientGuard],
})
export class OAuthClientModule {}

View File

@ -54,15 +54,15 @@ export class TokensRepository {
]);
return {
access_token: accessToken.secret,
refresh_token: refreshToken.secret,
accessToken: accessToken.secret,
refreshToken: refreshToken.secret,
};
}
async getAccessTokenExpiryDate(secret: string) {
async getAccessTokenExpiryDate(accessTokenSecret: string) {
const accessToken = await this.dbRead.prisma.accessToken.findFirst({
where: {
secret,
secret: accessTokenSecret,
},
select: {
expiresAt: true,
@ -71,6 +71,19 @@ export class TokensRepository {
return accessToken?.expiresAt;
}
async getAccessTokenOwnerId(accessTokenSecret: string) {
const accessToken = await this.dbRead.prisma.accessToken.findFirst({
where: {
secret: accessTokenSecret,
},
select: {
userId: true,
},
});
return accessToken?.userId;
}
async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) {
const accessExpiry = DateTime.now().plus({ days: 1 }).startOf("day").toJSDate();
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();

View File

@ -1,10 +0,0 @@
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UserRepository } from "@/modules/user/user.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [UserRepository],
exports: [UserRepository],
})
export class UserModule {}

View File

@ -0,0 +1,10 @@
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [UsersRepository],
exports: [UsersRepository],
})
export class UsersModule {}

View File

@ -1,12 +1,12 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { CreateUserInput } from "@/modules/user/input/create-user";
import { UpdateUserInput } from "@/modules/user/input/update-user";
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { UpdateUserInput } from "@/modules/users/inputs/update-user.input";
import { Injectable } from "@nestjs/common";
import type { User } from "@prisma/client";
@Injectable()
export class UserRepository {
export class UsersRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async create(user: CreateUserInput, oAuthClientId: string) {
@ -19,7 +19,7 @@ export class UserRepository {
},
});
return this.sanitize(newUser);
return this.sanitize(newUser, ["password"]);
}
async findById(userId: number) {
@ -30,7 +30,7 @@ export class UserRepository {
});
if (user) {
return this.sanitize(user);
return this.sanitize(user, ["password"]);
}
return null;
@ -44,7 +44,7 @@ export class UserRepository {
});
if (user) {
return this.sanitize(user);
return this.sanitize(user, ["password"]);
}
return null;
@ -55,7 +55,7 @@ export class UserRepository {
where: { id: userId },
data: updateData,
});
return this.sanitize(updatedUser);
return this.sanitize(updatedUser, ["password"]);
}
async delete(userId: number): Promise<User> {
@ -64,8 +64,13 @@ export class UserRepository {
});
}
sanitize(user: User): Partial<User> {
const keys: (keyof User)[] = ["password"];
return Object.fromEntries(Object.entries(user).filter(([key]) => !keys.includes(key as keyof User)));
sanitize<T extends keyof User>(user: User, keys: T[]): Omit<User, T> {
const sanitizedUser = { ...user };
keys.forEach((key) => {
delete sanitizedUser[key];
});
return sanitizedUser;
}
}

View File

@ -1,4 +1,4 @@
import { UserRepository } from "@/modules/user/user.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
@ -9,14 +9,14 @@ class BaseStrategy {
@Injectable()
export class NextAuthMockStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
constructor(private readonly email: string, private readonly userRepository: UserRepository) {
constructor(private readonly email: string, private readonly userRepository: UsersRepository) {
super();
}
async authenticate() {
try {
const user = await this.userRepository.findByEmail(this.email);
if (!user) {
throw new Error("User not found");
throw new Error("User with the provided email not found");
}
return this.success(user);

View File

@ -1,11 +1,10 @@
import { NextAuthStrategy } from "@/modules/auth/strategy";
import { UserRepository } from "@/modules/user/user.repository";
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
import { UsersRepository } from "@/modules/users/users.repository";
import { TestingModuleBuilder } from "@nestjs/testing";
import { NextAuthMockStrategy } from "../mocks/next-auth-mock.strategy";
import { NextAuthMockStrategy } from "test/mocks/next-auth-mock.strategy";
export const withNextAuth = (email: string, module: TestingModuleBuilder) =>
module.overrideProvider(NextAuthStrategy).useFactory({
factory: (userRepository: UserRepository) => new NextAuthMockStrategy(email, userRepository),
inject: [UserRepository],
factory: (userRepository: UsersRepository) => new NextAuthMockStrategy(email, userRepository),
inject: [UsersRepository],
});

View File

@ -11,7 +11,7 @@ import { hasPermission } from "../../../../../../../../packages/platform/utils/p
type OAuthClientCardProps = {
name: string;
logo?: Avatar;
redirect_uris: string[];
redirectUris: string[];
permissions: number;
lastItem: boolean;
id: string;
@ -23,7 +23,7 @@ type OAuthClientCardProps = {
export const OAuthClientCard = ({
name,
logo,
redirect_uris,
redirectUris,
permissions,
id,
secret,
@ -107,7 +107,7 @@ export const OAuthClientCard = ({
</div>
<div className="flex gap-1 text-sm">
<span className="font-semibold">Redirect uris: </span>
{redirect_uris.map((item, index) => (redirect_uris.length === index + 1 ? `${item}` : `${item}, `))}
{redirectUris.map((item, index) => (redirectUris.length === index + 1 ? `${item}` : `${item}, `))}
</div>
</div>
<div className="flex items-center">

View File

@ -11,8 +11,8 @@ import { Meta, Button, TextField } from "@calcom/ui";
type FormValues = {
name: string;
logo?: string;
redirect_uri: string;
redirect_uris: string[];
redirectUri: string;
redirectUris: string[];
permissions: number;
eventTypeRead: boolean;
eventTypeWrite: boolean;
@ -51,7 +51,7 @@ export const OAuthClientForm: FC = () => {
name: data.name,
permissions: userPermissions,
// logo: data.logo,
redirect_uris: [data.redirect_uri],
redirectUris: [data.redirectUri],
});
};
@ -128,7 +128,7 @@ export const OAuthClientForm: FC = () => {
/>
</div> */}
<div className="mt-6">
<TextField label="Redirect uri" required={true} {...register("redirect_uri")} />
<TextField label="Redirect uri" required={true} {...register("redirectUri")} />
</div>
<div className="mt-6">
<h1 className="text-base font-semibold underline">Permissions</h1>

View File

@ -19,7 +19,7 @@ export const useCreateOAuthClient = (
}
) => {
const mutation = useMutation<
ApiResponse<{ client_id: string; client_secret: string }>,
ApiResponse<{ clientId: string; clientSecret: string }>,
unknown,
CreateOAuthClientInput
>({

View File

@ -34,5 +34,4 @@ export const API_ERROR_CODES = [
] as const;
// Request headers
export const X_CAL_CLIENT_ID = "x-cal-client-id";
export const X_CAL_SECRET_KEY = "x-cal-secret-key";

View File

@ -10,7 +10,7 @@ export class CreateOAuthClientInput {
@IsArray()
@IsString({ each: true })
redirect_uris!: string[];
redirectUris!: string[];
@IsNumber()
permissions!: number;

View File

@ -1051,13 +1051,17 @@ model PlatformOAuthClient {
permissions Int
users User[]
logo String?
redirect_uris String[]
organizationId Int
redirectUris String[] @map("redirect_uris")
organizationId Int @map("organization_id")
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
accessTokens AccessToken[]
refreshToken RefreshToken[]
authorizationTokens PlatformAuthorizationToken[]
createdAt DateTime @default(now()) @map("created_at")
@@map(name: "platform_oauth_clients")
}
model PlatformAuthorizationToken {
@ -1066,8 +1070,10 @@ model PlatformAuthorizationToken {
owner User @relation(fields: [userId], references: [id])
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id])
platformOAuthClientId String
userId Int
platformOAuthClientId String @map("platform_oauth_client_id")
userId Int @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
@@unique([userId, platformOAuthClientId])
@@map("platform_authorization_token")
@ -1077,14 +1083,14 @@ model AccessToken {
id Int @id @default(autoincrement())
secret String @unique
createdAt DateTime @default(now())
expiresAt DateTime
createdAt DateTime @default(now()) @map("created_at")
expiresAt DateTime @map("expires_at")
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
platformOAuthClientId String
userId Int
platformOAuthClientId String @map("platform_oauth_client_id")
userId Int @map("user_id")
@@map("platform_access_tokens")
}
@ -1093,14 +1099,14 @@ model RefreshToken {
id Int @id @default(autoincrement())
secret String @unique
createdAt DateTime @default(now())
expiresAt DateTime
createdAt DateTime @default(now()) @map("created_at")
expiresAt DateTime @map("expires_at")
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
platformOAuthClientId String
userId Int
platformOAuthClientId String @map("platform_oauth_client_id")
userId Int @map("user_id")
@@map("platform_refresh_token")
}