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:
parent
e18eb6bc4b
commit
d4c946a0d6
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./get-user/get-user.decorator";
|
||||
export * from "./roles/roles.decorator";
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./next-auth/next-auth.guard";
|
||||
export * from "./organization-roles/organization-roles.guard";
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
export class AccessTokenGuard extends AuthGuard("access-token") {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./next-auth/next-auth.strategy";
|
||||
export * from "./api-key-auth/api-key-auth.strategy";
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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";
|
|
@ -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(
|
|
@ -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
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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) {
|
|
@ -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";
|
|
@ -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);
|
|
@ -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 () => {
|
|
@ -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 };
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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";
|
|
@ -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;
|
|
@ -2,8 +2,5 @@ import { IsString } from "class-validator";
|
|||
|
||||
export class OAuthAuthorizeInput {
|
||||
@IsString()
|
||||
redirect_uri!: string;
|
||||
|
||||
@IsString()
|
||||
client_id!: string;
|
||||
redirectUri!: string;
|
||||
}
|
|
@ -2,8 +2,5 @@ import { IsString } from "class-validator";
|
|||
|
||||
export class ExchangeAuthorizationCodeInput {
|
||||
@IsString()
|
||||
client_id!: string;
|
||||
|
||||
@IsString()
|
||||
client_secret!: string;
|
||||
clientSecret!: string;
|
||||
}
|
|
@ -2,5 +2,5 @@ import { IsString } from "class-validator";
|
|||
|
||||
export class RefreshTokenInput {
|
||||
@IsString()
|
||||
refresh_token!: string;
|
||||
refreshToken!: string;
|
||||
}
|
|
@ -12,5 +12,5 @@ export class UpdateOAuthClientInput {
|
|||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
redirect_uris?: string[] = [];
|
||||
redirectUris?: string[] = [];
|
||||
}
|
|
@ -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 {}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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();
|
||||
|
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -19,7 +19,7 @@ export const useCreateOAuthClient = (
|
|||
}
|
||||
) => {
|
||||
const mutation = useMutation<
|
||||
ApiResponse<{ client_id: string; client_secret: string }>,
|
||||
ApiResponse<{ clientId: string; clientSecret: string }>,
|
||||
unknown,
|
||||
CreateOAuthClientInput
|
||||
>({
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -10,7 +10,7 @@ export class CreateOAuthClientInput {
|
|||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
redirect_uris!: string[];
|
||||
redirectUris!: string[];
|
||||
|
||||
@IsNumber()
|
||||
permissions!: number;
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user