diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts index 20743bda70..e521f05a29 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts @@ -305,8 +305,6 @@ describe("OAuth Client Users Endpoints", () => { expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat); expect(responseBody.data.user.locale).toEqual(requestBody.locale); expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl); - expect(responseBody.data.user.bio).toEqual(requestBody.bio); - expect(responseBody.data.user.metadata).toEqual(requestBody.metadata); const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = response.body.data; expect(accessToken).toBeDefined(); @@ -372,8 +370,6 @@ describe("OAuth Client Users Endpoints", () => { expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat); expect(responseBody.data.user.locale).toEqual(requestBody.locale); expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl); - expect(responseBody.data.user.bio).toEqual(requestBody.bio); - expect(responseBody.data.user.metadata).toEqual(requestBody.metadata); const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = response.body.data; expect(accessToken).toBeDefined(); diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index fae4f35cae..6660228cf5 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -98,7 +98,10 @@ export class OAuthClientUsersController { status: SUCCESS_STATUS, data: { user: this.oAuthClientUsersOutputService.getResponseUser(user), - ...tokens, + accessToken: tokens.accessToken, + accessTokenExpiresAt: tokens.accessTokenExpiresAt.valueOf(), + refreshToken: tokens.refreshToken, + refreshTokenExpiresAt: tokens.refreshTokenExpiresAt.valueOf(), }, }; } @@ -162,7 +165,7 @@ export class OAuthClientUsersController { @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Force refresh tokens", - description: `If you have lost managed user access or refresh token or the refresh token has expired, then you can get new ones by using OAuth credentials. ${TOKENS_DOCS}`, + description: `If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials. ${TOKENS_DOCS}`, }) @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER]) async forceRefresh( @@ -171,16 +174,19 @@ export class OAuthClientUsersController { ): Promise { this.logger.log(`Forcing new access tokens for managed user with ID ${userId}`); - await this.validateManagedUserOwnership(oAuthClientId, userId); + const { id } = await this.validateManagedUserOwnership(oAuthClientId, userId); - const { accessToken, refreshToken } = await this.tokensRepository.refreshOAuthTokens( - userId, - oAuthClientId - ); + const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = + await this.tokensRepository.createOAuthTokens(oAuthClientId, id, true); return { status: SUCCESS_STATUS, - data: this.oAuthClientUsersService.getResponseOAuthTokens(accessToken, refreshToken), + data: { + accessToken, + refreshToken, + accessTokenExpiresAt: accessTokenExpiresAt.valueOf(), + refreshTokenExpiresAt: refreshTokenExpiresAt.valueOf(), + }, }; } diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts index 72e5aab4fd..eb5bf0c419 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts @@ -18,6 +18,7 @@ import * as request from "supertest"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; import { withNextAuth } from "test/utils/withNextAuth"; @@ -204,12 +205,14 @@ describe("OAuthFlow Endpoints", () => { expect(decodedAccessToken.clientId).toBe(oAuthClient.id); expect(decodedAccessToken.ownerId).toBe(user.id); + expect(decodedAccessToken.userId).toBe(user.id); expect(decodedAccessToken.type).toBe("access_token"); expect(decodedAccessToken.expiresAt).toBe(new Date(accessTokenExpiresAt).valueOf()); expect(decodedAccessToken.iat).toBeGreaterThan(0); expect(decodedRefreshToken.clientId).toBe(oAuthClient.id); expect(decodedRefreshToken.ownerId).toBe(user.id); + expect(decodedRefreshToken.userId).toBe(user.id); expect(decodedRefreshToken.type).toBe("refresh_token"); expect(decodedRefreshToken.expiresAt).toBe(new Date(refreshTokenExpiresAt).valueOf()); expect(decodedRefreshToken.iat).toBeGreaterThan(0); diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts index 5cf932d52d..2dc5a3947a 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts @@ -33,8 +33,7 @@ import { Response as ExpressResponse } from "express"; import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; export const TOKENS_DOCS = `Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model \`calAccessToken\` and \`calRefreshToken\` fields. -Response also contains \`accessTokenExpiresAt\` and \`refreshTokenExpiresAt\` fields, but if you decode the jwt token the payload will contain \`clientId\` (OAuth client ID), \`ownerId\` (user to whom token belongs ID), \`iat\` (issued at time) and \`expiresAt\` (when does the token expire) fields. -To learn about managing refresh tokens expiring after 1 year follow [this guide](https://cal.com/docs/platform/quickstart#5-managing-refresh-token-expiry)`; +Response also contains \`accessTokenExpiresAt\` and \`refreshTokenExpiresAt\` fields, but if you decode the jwt token the payload will contain \`clientId\` (OAuth client ID), \`ownerId\` (user to whom token belongs ID), \`iat\` (issued at time) and \`expiresAt\` (when does the token expire) fields.`; @Controller({ path: "/v2/oauth/:clientId", @@ -95,7 +94,7 @@ export class OAuthFlowController { throw new BadRequestException("Missing 'Bearer' Authorization header."); } - const tokens = await this.oAuthFlowService.exchangeOAuthClientAuthorizationToken( + const tokens = await this.oAuthFlowService.exchangeAuthorizationToken( authorizeEndpointCode, clientId, body.clientSecret @@ -126,7 +125,7 @@ export class OAuthFlowController { @Headers(X_CAL_SECRET_KEY) secretKey: string, @Body() body: RefreshTokenInput ): Promise { - const tokens = await this.oAuthFlowService.refreshUserTokens(clientId, secretKey, body.refreshToken); + const tokens = await this.oAuthFlowService.refreshToken(clientId, secretKey, body.refreshToken); return { status: SUCCESS_STATUS, diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index 140723e34e..8ac4bef7b2 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -3,13 +3,12 @@ import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_ import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; import { Locales } from "@/lib/enums/locales"; import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input"; -import { KeysDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input"; import { UsersRepository } from "@/modules/users/users.repository"; import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common"; -import { User, CreationSource, PlatformOAuthClient, AccessToken, RefreshToken } from "@prisma/client"; +import { User, CreationSource, PlatformOAuthClient } from "@prisma/client"; import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries"; @@ -79,10 +78,8 @@ export class OAuthClientUsersService { }); } - const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens( - user.id, - oAuthClientId - ); + const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = + await this.tokensRepository.createOAuthTokens(oAuthClientId, user.id); if (oAuthClient.areDefaultEventTypesEnabled) { await this.eventTypesService.createUserDefaultEventTypes(user.id); @@ -102,16 +99,12 @@ export class OAuthClientUsersService { return { user, - tokens: this.getResponseOAuthTokens(accessToken, refreshToken), - }; - } - - getResponseOAuthTokens(accessToken: AccessToken, refreshToken: RefreshToken): KeysDto { - return { - accessToken: accessToken.secret, - refreshToken: refreshToken.secret, - accessTokenExpiresAt: accessToken.expiresAt.valueOf(), - refreshTokenExpiresAt: refreshToken.expiresAt.valueOf(), + tokens: { + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + }, }; } diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts index a95dfa2b42..253fb18b46 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts @@ -1,5 +1,8 @@ import { TokenExpiredException } from "@/modules/auth/guards/api-auth/token-expired.exception"; -import { KeysDto } from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto"; +import { + KeysDto, + KeysResponseDto, +} from "@/modules/oauth-clients/controllers/oauth-flow/responses/KeysResponse.dto"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { RedisService } from "@/modules/redis/redis.service"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; @@ -7,7 +10,6 @@ import { BadRequestException, Injectable, Logger, UnauthorizedException } from " import { DateTime } from "luxon"; import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants"; -import { AccessToken, RefreshToken } from "@calcom/prisma/client"; @Injectable() export class OAuthFlowService { @@ -57,8 +59,8 @@ export class OAuthFlowService { if (!ownerIdFromDb) throw new Error("Invalid Access Token, not present in Redis or DB"); - // await in case of race conditions - await this.redisService.redis.setex(cacheKey, 3600, ownerIdFromDb); // expires in 1 hour + // await in case of race conditions, but void it's return since cache writes shouldn't halt execution. + void (await this.redisService.redis.setex(cacheKey, 3600, ownerIdFromDb)); // expires in 1 hour return ownerIdFromDb; } @@ -83,8 +85,9 @@ export class OAuthFlowService { } // we can't use a Promise#all or similar here because we care about execution order - await this.redisService.redis.hmset(cacheKey, { expiresAt: tokenExpiresAt.toJSON() }); - await this.redisService.redis.expireat(cacheKey, Math.floor(tokenExpiresAt.getTime() / 1000)); + // however we can't allow caches to fail a validation hence the results are voided. + void (await this.redisService.redis.hmset(cacheKey, { expiresAt: tokenExpiresAt.toJSON() })); + void (await this.redisService.redis.expireat(cacheKey, Math.floor(tokenExpiresAt.getTime() / 1000))); return true; } @@ -100,7 +103,7 @@ export class OAuthFlowService { return { status: "CACHE_MISS", cacheKey }; } - async exchangeOAuthClientAuthorizationToken( + async exchangeAuthorizationToken( tokenId: string, clientId: string, clientSecret: string @@ -121,50 +124,46 @@ export class OAuthFlowService { throw new BadRequestException("Invalid Authorization Token."); } - const { accessToken, refreshToken } = await this.tokensRepository.createOAuthTokens( - authorizationToken.owner.id, - clientId - ); + const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } = + await this.tokensRepository.createOAuthTokens(clientId, authorizationToken.owner.id); await this.tokensRepository.invalidateAuthorizationToken(authorizationToken.id); - await this.propagateAccessToken(accessToken.secret); + void this.propagateAccessToken(accessToken); // void result, ignored. - return this.getResponseOAuthTokens(accessToken, refreshToken); + return { + accessToken, + accessTokenExpiresAt: accessTokenExpiresAt.valueOf(), + refreshToken, + refreshTokenExpiresAt: refreshTokenExpiresAt.valueOf(), + }; } - async refreshUserTokens( - clientId: string, - clientSecret: string, - userRefreshTokenSecret: string - ): Promise { + async refreshToken(clientId: string, clientSecret: string, tokenSecret: string): Promise { const oauthClient = await this.oAuthClientRepository.getOAuthClientWithRefreshSecret( clientId, clientSecret, - userRefreshTokenSecret + tokenSecret ); if (!oauthClient) { throw new BadRequestException("Invalid OAuthClient credentials."); } - const currentUserRefreshToken = oauthClient.refreshToken[0]; + const currentRefreshToken = oauthClient.refreshToken[0]; - if (!currentUserRefreshToken) { - throw new BadRequestException("Invalid managed user refresh token"); + if (!currentRefreshToken) { + throw new BadRequestException("Invalid refresh token"); } const { accessToken, refreshToken } = await this.tokensRepository.refreshOAuthTokens( - currentUserRefreshToken.userId, - clientId + clientId, + currentRefreshToken.secret, + currentRefreshToken.userId ); - return this.getResponseOAuthTokens(accessToken, refreshToken); - } - - getResponseOAuthTokens(accessToken: AccessToken, refreshToken: RefreshToken): KeysDto { return { accessToken: accessToken.secret, - refreshToken: refreshToken.secret, accessTokenExpiresAt: accessToken.expiresAt.valueOf(), + refreshToken: refreshToken.secret, refreshTokenExpiresAt: refreshToken.expiresAt.valueOf(), }; } diff --git a/apps/api/v2/src/modules/tokens/tokens.repository.ts b/apps/api/v2/src/modules/tokens/tokens.repository.ts index b21a3a34a6..e492cd1710 100644 --- a/apps/api/v2/src/modules/tokens/tokens.repository.ts +++ b/apps/api/v2/src/modules/tokens/tokens.repository.ts @@ -50,6 +50,54 @@ export class TokensRepository { }); } + async createOAuthTokens(clientId: string, ownerId: number, deleteOld?: boolean) { + if (deleteOld) { + try { + await this.dbWrite.prisma.$transaction([ + this.dbWrite.prisma.accessToken.deleteMany({ + where: { client: { id: clientId }, userId: ownerId }, + }), + this.dbWrite.prisma.refreshToken.deleteMany({ + where: { + client: { id: clientId }, + userId: ownerId, + }, + }), + ]); + } catch (err) { + this.logger.error("createOAuthTokens - Failed to delete old tokens", err); + } + } + + const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate(); + const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate(); + const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([ + this.dbWrite.prisma.accessToken.create({ + data: { + secret: this.jwtService.signAccessToken({ clientId, ownerId, expiresAt: accessExpiry.valueOf() }), + expiresAt: accessExpiry, + client: { connect: { id: clientId } }, + owner: { connect: { id: ownerId } }, + }, + }), + this.dbWrite.prisma.refreshToken.create({ + data: { + secret: this.jwtService.signRefreshToken({ clientId, ownerId, expiresAt: refreshExpiry.valueOf() }), + expiresAt: refreshExpiry, + client: { connect: { id: clientId } }, + owner: { connect: { id: ownerId } }, + }, + }), + ]); + + return { + accessToken: accessToken.secret, + accessTokenExpiresAt: accessToken.expiresAt, + refreshToken: refreshToken.secret, + refreshTokenExpiresAt: refreshToken.expiresAt, + }; + } + async getAccessTokenExpiryDate(accessTokenSecret: string) { const accessToken = await this.dbRead.prisma.accessToken.findFirst({ where: { @@ -75,57 +123,46 @@ export class TokensRepository { return accessToken?.userId; } - async refreshOAuthTokens(ownerId: number, clientId: string) { - try { - await this.deleteOAuthTokens(ownerId, clientId); - } catch (err) { - this.logger.error("refreshOAuthTokens - Failed to delete old tokens", err); - } - - return await this.createOAuthTokens(ownerId, clientId); - } - - private async deleteOAuthTokens(ownerId: number, clientId: string) { - await this.dbWrite.prisma.$transaction([ - this.dbWrite.prisma.accessToken.deleteMany({ - where: { client: { id: clientId }, userId: ownerId }, - }), - this.dbWrite.prisma.refreshToken.deleteMany({ - where: { - client: { id: clientId }, - userId: ownerId, - }, - }), - ]); - } - - async createOAuthTokens(ownerId: number, clientId: string) { + async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) { const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate(); const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate(); - const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, _refresh, accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([ + this.dbWrite.prisma.accessToken.deleteMany({ + where: { client: { id: clientId }, expiresAt: { lte: new Date() } }, + }), + this.dbWrite.prisma.refreshToken.delete({ where: { secret: refreshTokenSecret } }), this.dbWrite.prisma.accessToken.create({ data: { - secret: this.jwtService.signAccessToken({ clientId, ownerId, expiresAt: accessExpiry.valueOf() }), + secret: this.jwtService.signAccessToken({ + clientId, + ownerId: tokenUserId, + expiresAt: accessExpiry.valueOf(), + userId: tokenUserId, + }), expiresAt: accessExpiry, client: { connect: { id: clientId } }, - owner: { connect: { id: ownerId } }, + owner: { connect: { id: tokenUserId } }, }, }), this.dbWrite.prisma.refreshToken.create({ data: { - secret: this.jwtService.signRefreshToken({ clientId, ownerId, expiresAt: refreshExpiry.valueOf() }), + // note(Lauris): I am leaving userId because it was before adding ownerId to standardize payload like in the createOAuthTokens function for + // backwards compatability. + secret: this.jwtService.signRefreshToken({ + clientId, + ownerId: tokenUserId, + expiresAt: refreshExpiry.valueOf(), + userId: tokenUserId, + }), expiresAt: refreshExpiry, client: { connect: { id: clientId } }, - owner: { connect: { id: ownerId } }, + owner: { connect: { id: tokenUserId } }, }, }), ]); - - return { - accessToken, - refreshToken, - }; + return { accessToken, refreshToken }; } async getAccessTokenClient(accessToken: string) { diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index dfcbe4ff63..483ae91ebe 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -276,7 +276,7 @@ "post": { "operationId": "OAuthClientUsersController_forceRefresh", "summary": "Force refresh tokens", - "description": "If you have lost managed user access or refresh token or the refresh token has expired, then you can get new ones by using OAuth credentials. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.\nTo learn about managing refresh tokens expiring after 1 year follow [this guide](https://cal.com/docs/platform/quickstart#5-managing-refresh-token-expiry)", + "description": "If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.", "parameters": [ { "name": "x-cal-secret-key", @@ -325,7 +325,7 @@ "post": { "operationId": "OAuthFlowController_refreshTokens", "summary": "Refresh managed user tokens", - "description": "If managed user access token is expired then get a new one using this endpoint - it will also refresh the refresh token, because we use\n \"refresh token rotation\" mechanism. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.\nTo learn about managing refresh tokens expiring after 1 year follow [this guide](https://cal.com/docs/platform/quickstart#5-managing-refresh-token-expiry)", + "description": "If managed user access token is expired then get a new one using this endpoint - it will also refresh the refresh token, because we use\n \"refresh token rotation\" mechanism. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.", "parameters": [ { "name": "clientId", diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index c8053949e7..9973d103c9 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -266,7 +266,7 @@ "post": { "operationId": "OAuthClientUsersController_forceRefresh", "summary": "Force refresh tokens", - "description": "If you have lost managed user access or refresh token or the refresh token has expired, then you can get new ones by using OAuth credentials. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.\nTo learn about managing refresh tokens expiring after 1 year follow [this guide](https://cal.com/docs/platform/quickstart#5-managing-refresh-token-expiry)", + "description": "If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.", "parameters": [ { "name": "x-cal-secret-key", @@ -313,7 +313,7 @@ "post": { "operationId": "OAuthFlowController_refreshTokens", "summary": "Refresh managed user tokens", - "description": "If managed user access token is expired then get a new one using this endpoint - it will also refresh the refresh token, because we use\n \"refresh token rotation\" mechanism. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.\nTo learn about managing refresh tokens expiring after 1 year follow [this guide](https://cal.com/docs/platform/quickstart#5-managing-refresh-token-expiry)", + "description": "If managed user access token is expired then get a new one using this endpoint - it will also refresh the refresh token, because we use\n \"refresh token rotation\" mechanism. Access token is valid for 60 minutes and refresh token for 1 year. Make sure to store them in your database, for example, in your User database model `calAccessToken` and `calRefreshToken` fields.\nResponse also contains `accessTokenExpiresAt` and `refreshTokenExpiresAt` fields, but if you decode the jwt token the payload will contain `clientId` (OAuth client ID), `ownerId` (user to whom token belongs ID), `iat` (issued at time) and `expiresAt` (when does the token expire) fields.", "parameters": [ { "name": "clientId", diff --git a/packages/platform/examples/base/src/pages/booking.tsx b/packages/platform/examples/base/src/pages/booking.tsx index c9863ec9ac..502365ea6d 100644 --- a/packages/platform/examples/base/src/pages/booking.tsx +++ b/packages/platform/examples/base/src/pages/booking.tsx @@ -131,7 +131,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string ? { isTeamEvent: true, teamId: teams?.[0]?.id || 0 } : { username: props.calUsername })} hostsLimit={3} - // selectedDate={new Date("2025-03-25")} + selectedDate={new Date("2025-03-25")} allowUpdatingUrlParams={true} />