Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4db3a75a8 | ||
|
|
a9fcd900cd | ||
|
|
fcb443a8eb | ||
|
|
9dfa596e3e | ||
|
|
0e8ac7e4ed | ||
|
|
b236bffca2 | ||
|
|
6e829e23f9 | ||
|
|
b778b2962a | ||
|
|
0d3baee593 | ||
|
|
f9dcbaaa42 | ||
|
|
60700e1a7a | ||
|
|
456adea570 | ||
|
|
63abda1693 | ||
|
|
4dd73f4d86 | ||
|
|
31caf8288d | ||
|
|
99e71365ab | ||
|
|
943c7a4c6c | ||
|
|
228e509a3b | ||
|
|
eb909cc87a | ||
|
|
65a36b37c5 | ||
|
|
2799ddf3a4 | ||
|
|
3164cd4ae7 | ||
|
|
6b26dbc6da | ||
|
|
6b09c6b3b4 | ||
|
|
2352bb1075 | ||
|
|
a568e1ac66 | ||
|
|
add6ffdfc4 | ||
|
|
7a67331d96 | ||
|
|
1aa8b8439a | ||
|
|
50e405353c | ||
|
|
565e21bc50 | ||
|
|
098d41fc62 | ||
|
|
a20179727b | ||
|
|
795aaca64f | ||
|
|
2ab7846a4a | ||
|
|
e9ea0fcc20 | ||
|
|
090b166b1c | ||
|
|
ea2d8fc873 | ||
|
|
46c51df1e0 | ||
|
|
5fe919452b | ||
|
|
83c8f97afd | ||
|
|
f87eac193f | ||
|
|
c53e891c8a | ||
|
|
90a6fc3f26 | ||
|
|
969411041b | ||
|
|
518cfbc037 | ||
|
|
47277ced2d | ||
|
|
db625157d1 | ||
|
|
bf2af799d5 | ||
|
|
75eaed1c4d | ||
|
|
e1ac6f5454 | ||
|
|
55d44ce789 | ||
|
|
8bed690ed6 | ||
|
|
adf012643a | ||
|
|
ca0d23529a | ||
|
|
9f50941904 | ||
|
|
f2a59fe4e8 | ||
|
|
325f250d39 | ||
|
|
bb7057ea04 | ||
|
|
d185d7f7d8 | ||
|
|
80b92b5f11 | ||
|
|
9a6d4e63e8 | ||
|
|
4062ae8486 | ||
|
|
c6f26f2a2a | ||
|
|
2f4b1818d0 | ||
|
|
8e1ce633fb | ||
|
|
44e4f4c644 | ||
|
|
0b9cf78e9b | ||
|
|
b2cdb08780 | ||
|
|
dcf9c6a5e5 | ||
|
|
ca78be011c | ||
|
|
d13dedda9a | ||
|
|
9ca2ad2d41 | ||
|
|
3d89809e67 | ||
|
|
252ce07861 | ||
|
|
a75ef8870b | ||
|
|
b840b3b1bb | ||
|
|
2e50625165 | ||
|
|
c78eb752a7 | ||
|
|
1c20bdcecf | ||
|
|
338756439f | ||
|
|
56050b994d | ||
|
|
c11ecbb323 | ||
|
|
7f23ae156b | ||
|
|
33250652b3 | ||
|
|
a65e18d92d | ||
|
|
d0f7085cb8 | ||
|
|
ea8437b4f7 | ||
|
|
877cd4cdff | ||
|
|
bae3bd76e5 | ||
|
|
8b24995d52 | ||
|
|
7dc6df2ad6 | ||
|
|
6c4b1154b8 | ||
|
|
1099bad930 | ||
|
|
dbd5d27d9d | ||
|
|
45c4f4789b | ||
|
|
07f42057fc | ||
|
|
79ee7c41b2 | ||
|
|
57e6971942 | ||
|
|
bd6ca21e02 | ||
|
|
f19a1926b4 | ||
|
|
477ee1c526 | ||
|
|
6704e18e32 | ||
|
|
20f17903b9 | ||
|
|
8dabbbd09f | ||
|
|
32933ed6ef | ||
|
|
c765ea9b31 | ||
|
|
d3ddbf418f | ||
|
|
79d0513428 | ||
|
|
230b82d3bf | ||
|
|
c9b50ffb78 | ||
|
|
ada0ef242b | ||
|
|
b26e688950 | ||
|
|
d9041d03b7 | ||
|
|
f60b3c14e1 | ||
|
|
fbcc43931e | ||
|
|
a48e7cb39a | ||
|
|
db3f718ba6 | ||
|
|
ca5dd4626d | ||
|
|
4524d722f6 | ||
|
|
2094d59856 | ||
|
|
7b350a5b8f | ||
|
|
5a869228d3 | ||
|
|
dabd5eae73 | ||
|
|
cb78de231a | ||
|
|
b0a1ef8a49 | ||
|
|
0a43aa3351 | ||
|
|
ed4cce1c7e | ||
|
|
d595aa23c0 | ||
|
|
a6df49235f | ||
|
|
5df41e37a0 | ||
|
|
fca5778b6d | ||
|
|
0ddefaa141 | ||
|
|
c11f7aeffc | ||
|
|
9903fcaa05 |
No files matched your search
@@ -37,6 +37,7 @@ BASECAMP3_USER_AGENT=
|
|||||||
|
|
||||||
DAILY_API_KEY=
|
DAILY_API_KEY=
|
||||||
DAILY_SCALE_PLAN=''
|
DAILY_SCALE_PLAN=''
|
||||||
|
DAILY_WEBHOOK_SECRET=''
|
||||||
|
|
||||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||||
# Needed to enable Google Calendar integration and Login with Google
|
# Needed to enable Google Calendar integration and Login with Google
|
||||||
|
|||||||
+15
-2
@@ -155,6 +155,7 @@ NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
|||||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
STRIPE_WEBHOOK_SECRET_APPS=
|
||||||
STRIPE_PRIVATE_KEY=
|
STRIPE_PRIVATE_KEY=
|
||||||
STRIPE_CLIENT_ID=
|
STRIPE_CLIENT_ID=
|
||||||
PAYMENT_FEE_FIXED=
|
PAYMENT_FEE_FIXED=
|
||||||
@@ -195,6 +196,10 @@ EMAIL_SERVER_PORT=1025
|
|||||||
# Make sure to run mailhog container manually or with `yarn dx`
|
# Make sure to run mailhog container manually or with `yarn dx`
|
||||||
E2E_TEST_MAILHOG_ENABLED=
|
E2E_TEST_MAILHOG_ENABLED=
|
||||||
|
|
||||||
|
# Resend
|
||||||
|
# Send transactional email using resend
|
||||||
|
# RESEND_API_KEY=
|
||||||
|
|
||||||
# **********************************************************************************************************
|
# **********************************************************************************************************
|
||||||
|
|
||||||
# Set the following value to true if you wish to enable Team Impersonation
|
# Set the following value to true if you wish to enable Team Impersonation
|
||||||
@@ -265,7 +270,7 @@ CALCOM_WEBHOOK_SECRET=""
|
|||||||
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
||||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||||
# Key should match on Cal.com and your application
|
# Key should match on Cal.com and your application
|
||||||
# must be 32 bytes for AES256 encryption algorithm
|
# must be 24 bytes for AES256 encryption algorithm
|
||||||
# You can use: `openssl rand -base64 24` to generate one
|
# You can use: `openssl rand -base64 24` to generate one
|
||||||
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""
|
||||||
|
|
||||||
@@ -288,4 +293,12 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
|||||||
# redirected from the legacy to the future pages
|
# redirected from the legacy to the future pages
|
||||||
AB_TEST_BUCKET_PROBABILITY=50
|
AB_TEST_BUCKET_PROBABILITY=50
|
||||||
# whether we redirect to the future/event-types from event-types or not
|
# whether we redirect to the future/event-types from event-types or not
|
||||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||||
|
APP_ROUTER_SETTINGS_ADMIN_ENABLED=0
|
||||||
|
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||||
|
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||||
|
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||||
|
# whether we redirect to the future/apps/categories from /apps/categories or not
|
||||||
|
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||||
|
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||||
|
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||||
@@ -23,6 +23,7 @@ Fixes # (issue)
|
|||||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] Tests (Unit/Integration/E2E or any other test)
|
||||||
- [ ] This change requires a documentation update
|
- [ ] This change requires a documentation update
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ runs:
|
|||||||
**/.turbo/**
|
**/.turbo/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||||
- run: yarn build
|
- run: |
|
||||||
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
|
yarn build
|
||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shard: [1, 2, 3, 4, 5]
|
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|||||||
@@ -44,5 +44,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
message: |
|
message: |
|
||||||
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link to [collect XP and win prizes!](https://cal.com/blog/community-incentives)
|
Thank you for following the naming conventions! 🙏 Feel free to join our [discord](https://go.cal.com/discord) and post your PR link.
|
||||||
|
|
||||||
+11
-11
@@ -1,15 +1,15 @@
|
|||||||
diff --git a/index.cjs b/index.cjs
|
diff --git a/index.cjs b/index.cjs
|
||||||
index b645707a3549fc298508726e404243499bbed499..f34b0891e99b275a9218e253f303f43d31ef3f73 100644
|
index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644
|
||||||
--- a/index.cjs
|
--- a/index.cjs
|
||||||
+++ b/index.cjs
|
+++ b/index.cjs
|
||||||
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
|
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
|
||||||
// https://github.com/babel/babel/issues/2212#issuecomment-131827986
|
// https://github.com/babel/babel/issues/2212#issuecomment-131827986
|
||||||
// An alternative approach:
|
// An alternative approach:
|
||||||
// https://www.npmjs.com/package/babel-plugin-add-module-exports
|
// https://www.npmjs.com/package/babel-plugin-add-module-exports
|
||||||
-exports = module.exports = min.parsePhoneNumberFromString
|
-exports = module.exports = min.parsePhoneNumberFromString
|
||||||
-exports['default'] = min.parsePhoneNumberFromString
|
-exports['default'] = min.parsePhoneNumberFromString
|
||||||
+// exports = module.exports = min.parsePhoneNumberFromString
|
+// exports = module.exports = min.parsePhoneNumberFromString
|
||||||
+// exports['default'] = min.parsePhoneNumberFromString
|
+// exports['default'] = min.parsePhoneNumberFromString
|
||||||
|
|
||||||
// `parsePhoneNumberFromString()` named export is now considered legacy:
|
// `parsePhoneNumberFromString()` named export is now considered legacy:
|
||||||
// it has been promoted to a default export due to being too verbose.
|
// it has been promoted to a default export due to being too verbose.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
The open-source Calendly alternative.
|
The open-source Calendly successor.
|
||||||
<br />
|
<br />
|
||||||
<a href="https://cal.com"><strong>Learn more »</strong></a>
|
<a href="https://cal.com"><strong>Learn more »</strong></a>
|
||||||
<br />
|
<br />
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
# Scheduling infrastructure for absolutely everyone
|
# Scheduling infrastructure for absolutely everyone
|
||||||
|
|
||||||
The open source Calendly alternative. You are in charge
|
The open source Calendly successor. You are in charge
|
||||||
of your own data, workflow, and appearance.
|
of your own data, workflow, and appearance.
|
||||||
|
|
||||||
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization.
|
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes, and even calls with our families. However, most tools are very limited in terms of control and customization.
|
||||||
@@ -221,7 +221,6 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
|
|
||||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||||
|
|
||||||
1. Set a 24 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
|
||||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||||
|
|
||||||
In a development environment, run:
|
In a development environment, run:
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
|||||||
successRedirectUrl: true,
|
successRedirectUrl: true,
|
||||||
locations: true,
|
locations: true,
|
||||||
bookingLimits: true,
|
bookingLimits: true,
|
||||||
|
onlyShowFirstAvailableSlot: true,
|
||||||
durationLimits: true,
|
durationLimits: true,
|
||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
@@ -147,6 +148,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||||||
seatsShowAvailabilityCount: true,
|
seatsShowAvailabilityCount: true,
|
||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
bookingLimits: true,
|
bookingLimits: true,
|
||||||
|
onlyShowFirstAvailableSlot: true,
|
||||||
durationLimits: true,
|
durationLimits: true,
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
|||||||
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
|
||||||
// for example making weekStart only accept weekdays as input
|
// for example making weekStart only accept weekdays as input
|
||||||
const schemaUserEditParams = z.object({
|
const schemaUserEditParams = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email().toLowerCase(),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
weekStart: z.nativeEnum(weekdays).optional(),
|
weekStart: z.nativeEnum(weekdays).optional(),
|
||||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
@@ -114,7 +114,7 @@ const schemaUserEditParams = z.object({
|
|||||||
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
|
||||||
|
|
||||||
const schemaUserCreateParams = z.object({
|
const schemaUserCreateParams = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email().toLowerCase(),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
weekStart: z.nativeEnum(weekdays).optional(),
|
weekStart: z.nativeEnum(weekdays).optional(),
|
||||||
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
|
||||||
|
|||||||
+15
-2
@@ -1,6 +1,8 @@
|
|||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
|
|
||||||
module.exports = withAxiom({
|
const plugins = [withAxiom];
|
||||||
|
const nextConfig = {
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@calcom/app-store",
|
"@calcom/app-store",
|
||||||
"@calcom/core",
|
"@calcom/core",
|
||||||
@@ -66,4 +68,15 @@ module.exports = withAxiom({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
|
nextConfig["sentry"] = {
|
||||||
|
autoInstrumentServerFunctions: true,
|
||||||
|
hideSourceMaps: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
plugins.push(withSentryConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
@@ -6,18 +6,44 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||||||
|
|
||||||
async function authMiddleware(req: NextApiRequest) {
|
async function authMiddleware(req: NextApiRequest) {
|
||||||
const { userId, prisma, isAdmin, query } = req;
|
const { userId, prisma, isAdmin, query } = req;
|
||||||
|
if (isAdmin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const userWithBookings = await prisma.user.findUnique({
|
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
include: { bookings: true },
|
include: {
|
||||||
|
bookings: true,
|
||||||
|
teams: {
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userWithBookings) throw new HttpError({ statusCode: 404, message: "User not found" });
|
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||||
|
|
||||||
const userBookingIds = userWithBookings.bookings.map((booking) => booking.id);
|
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
||||||
|
|
||||||
if (!isAdmin && !userBookingIds.includes(id)) {
|
if (!userBookingIds.includes(id)) {
|
||||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
const teamBookings = await prisma.booking.findUnique({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
eventType: {
|
||||||
|
team: {
|
||||||
|
id: {
|
||||||
|
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamBookings) {
|
||||||
|
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
@@ -89,7 +90,9 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
|||||||
if (req.isAdmin) return true;
|
if (req.isAdmin) return true;
|
||||||
if (eventType?.teamId) {
|
if (eventType?.teamId) {
|
||||||
req.query.teamId = String(eventType.teamId);
|
req.query.teamId = String(eventType.teamId);
|
||||||
await canAccessTeamEventOrThrow(req, "MEMBER");
|
await canAccessTeamEventOrThrow(req, {
|
||||||
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (eventType?.userId === req.userId) return true; // is owner.
|
if (eventType?.userId === req.userId) return true; // is owner.
|
||||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import type { NextApiRequest } from "next";
|
|||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
|
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||||
|
|
||||||
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||||
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
||||||
@@ -299,7 +301,7 @@ async function postHandler(req: NextApiRequest) {
|
|||||||
data.hosts = { createMany: { data: hosts } };
|
data.hosts = { createMany: { data: hosts } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventType = await prisma.eventType.create({ data });
|
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event_type: schemaEventTypeReadPublic.parse(eventType),
|
event_type: schemaEventTypeReadPublic.parse(eventType),
|
||||||
@@ -316,7 +318,13 @@ async function checkPermissions(req: NextApiRequest) {
|
|||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: "ADMIN required for `userId`",
|
message: "ADMIN required for `userId`",
|
||||||
});
|
});
|
||||||
if (!isAdmin && body.teamId)
|
if (
|
||||||
|
body.teamId &&
|
||||||
|
!isAdmin &&
|
||||||
|
!(await canUserAccessTeamWithRole(req.prisma, req.userId, isAdmin, body.teamId, {
|
||||||
|
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||||
|
}))
|
||||||
|
)
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: "ADMIN required for `teamId`",
|
message: "ADMIN required for `teamId`",
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
@@ -9,10 +13,34 @@ import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||||
|
|
||||||
|
// Apply plugins
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const input = getScheduleSchema.parse(req.query);
|
const { usernameList, ...rest } = req.query;
|
||||||
return await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
let slugs = usernameList;
|
||||||
|
if (!Array.isArray(usernameList)) {
|
||||||
|
slugs = usernameList ? [usernameList] : [];
|
||||||
|
}
|
||||||
|
const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
|
||||||
|
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
||||||
|
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||||
|
const slotsInProvidedTimeZone = timeZoneSupported
|
||||||
|
? Object.keys(availableSlots.slots).reduce(
|
||||||
|
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||||
|
acc[date] = availableSlots.slots[date].map((slot) => ({
|
||||||
|
...slot,
|
||||||
|
time: dayjs(slot.time).tz(input.timeZone).format(),
|
||||||
|
}));
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
: availableSlots;
|
||||||
|
|
||||||
|
return slotsInProvidedTimeZone;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (cause instanceof TRPCError) {
|
if (cause instanceof TRPCError) {
|
||||||
|
|||||||
@@ -22,7 +22,21 @@ export async function checkPermissions(
|
|||||||
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||||
) {
|
) {
|
||||||
const { userId, prisma, isAdmin } = req;
|
const { userId, prisma, isAdmin } = req;
|
||||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
const { teamId } = schemaQueryTeamId.parse({
|
||||||
|
teamId: req.query.teamId,
|
||||||
|
version: req.query.version,
|
||||||
|
apiKey: req.query.apiKey,
|
||||||
|
});
|
||||||
|
return canUserAccessTeamWithRole(prisma, userId, isAdmin, teamId, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function canUserAccessTeamWithRole(
|
||||||
|
prisma: NextApiRequest["prisma"],
|
||||||
|
userId: number,
|
||||||
|
isAdmin: boolean,
|
||||||
|
teamId: number,
|
||||||
|
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
|
||||||
|
) {
|
||||||
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
|
||||||
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
|
||||||
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
members: { some: { userId } },
|
members: { some: { userId } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
customInputs: true,
|
||||||
|
team: { select: { slug: true } },
|
||||||
|
users: true,
|
||||||
|
hosts: { select: { userId: true, isFixed: true } },
|
||||||
|
owner: { select: { username: true, id: true } },
|
||||||
|
children: { select: { id: true, userId: true } },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await prisma.eventType.findMany(args);
|
const data = await prisma.eventType.findMany(args);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
@@ -8,7 +8,9 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
storybook-static
|
storybook-static/*
|
||||||
|
!storybook-static/favicon.ico
|
||||||
|
!storybook-static/sb-cover.jpg
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
stories: [
|
|
||||||
"../intro.stories.mdx",
|
|
||||||
"../../../packages/ui/components/**/*.stories.mdx",
|
|
||||||
"../../../packages/atoms/**/*.stories.mdx",
|
|
||||||
"../../../packages/features/**/*.stories.mdx",
|
|
||||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
|
||||||
],
|
|
||||||
addons: [
|
|
||||||
"@storybook/addon-links",
|
|
||||||
"@storybook/addon-essentials",
|
|
||||||
"@storybook/addon-interactions",
|
|
||||||
"storybook-addon-rtl-direction",
|
|
||||||
"storybook-react-i18next",
|
|
||||||
"storybook-addon-next",
|
|
||||||
"storybook-addon-next-router",
|
|
||||||
/*{
|
|
||||||
name: "storybook-addon-next",
|
|
||||||
options: {
|
|
||||||
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
|
||||||
},
|
|
||||||
},*/
|
|
||||||
],
|
|
||||||
framework: "@storybook/react",
|
|
||||||
core: {
|
|
||||||
builder: "webpack5",
|
|
||||||
},
|
|
||||||
staticDirs: ["../public"],
|
|
||||||
webpackFinal: async (config, { configType }) => {
|
|
||||||
config.resolve.fallback = {
|
|
||||||
fs: false,
|
|
||||||
assert: false,
|
|
||||||
buffer: false,
|
|
||||||
console: false,
|
|
||||||
constants: false,
|
|
||||||
crypto: false,
|
|
||||||
domain: false,
|
|
||||||
events: false,
|
|
||||||
http: false,
|
|
||||||
https: false,
|
|
||||||
os: false,
|
|
||||||
path: false,
|
|
||||||
punycode: false,
|
|
||||||
process: false,
|
|
||||||
querystring: false,
|
|
||||||
stream: false,
|
|
||||||
string_decoder: false,
|
|
||||||
sys: false,
|
|
||||||
timers: false,
|
|
||||||
tty: false,
|
|
||||||
url: false,
|
|
||||||
util: false,
|
|
||||||
vm: false,
|
|
||||||
zlib: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [
|
|
||||||
"style-loader",
|
|
||||||
{
|
|
||||||
loader: "css-loader",
|
|
||||||
options: {
|
|
||||||
modules: true, // Enable modules to help you using className
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
include: path.resolve(__dirname, "../src"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
typescript: { reactDocgen: "react-docgen" },
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { StorybookConfig } from "@storybook/nextjs";
|
||||||
|
import path, { dirname, join } from "path";
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: [
|
||||||
|
"../intro.stories.mdx",
|
||||||
|
"../../../packages/ui/components/**/*.stories.mdx", // legacy SB6 stories
|
||||||
|
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
|
"../../../packages/ui/components/**/*.docs.mdx",
|
||||||
|
"../../../packages/features/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
|
"../../../packages/features/**/*.docs.mdx",
|
||||||
|
"../../../packages/atoms/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
|
"../../../packages/atoms/**/*.docs.mdx",
|
||||||
|
],
|
||||||
|
|
||||||
|
addons: [
|
||||||
|
getAbsolutePath("@storybook/addon-links"),
|
||||||
|
getAbsolutePath("@storybook/addon-essentials"),
|
||||||
|
getAbsolutePath("@storybook/addon-interactions"),
|
||||||
|
getAbsolutePath("storybook-addon-rtl-direction"),
|
||||||
|
getAbsolutePath("storybook-react-i18next"),
|
||||||
|
],
|
||||||
|
|
||||||
|
framework: {
|
||||||
|
name: getAbsolutePath("@storybook/nextjs") as "@storybook/nextjs",
|
||||||
|
|
||||||
|
options: {
|
||||||
|
// builder: {
|
||||||
|
// fsCache: true,
|
||||||
|
// lazyCompilation: true,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
staticDirs: ["../public"],
|
||||||
|
|
||||||
|
webpackFinal: async (config, { configType }) => {
|
||||||
|
config.resolve = config.resolve || {};
|
||||||
|
config.resolve.fallback = {
|
||||||
|
fs: false,
|
||||||
|
assert: false,
|
||||||
|
buffer: false,
|
||||||
|
console: false,
|
||||||
|
constants: false,
|
||||||
|
crypto: false,
|
||||||
|
domain: false,
|
||||||
|
events: false,
|
||||||
|
http: false,
|
||||||
|
https: false,
|
||||||
|
os: false,
|
||||||
|
path: false,
|
||||||
|
punycode: false,
|
||||||
|
process: false,
|
||||||
|
querystring: false,
|
||||||
|
stream: false,
|
||||||
|
string_decoder: false,
|
||||||
|
sys: false,
|
||||||
|
timers: false,
|
||||||
|
tty: false,
|
||||||
|
url: false,
|
||||||
|
util: false,
|
||||||
|
vm: false,
|
||||||
|
zlib: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
config.module = config.module || {};
|
||||||
|
config.module.rules = config.module.rules || [];
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
"style-loader",
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
modules: true, // Enable modules to help you using className
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
include: path.resolve(__dirname, "../src"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
typescript: { reactDocgen: "react-docgen" },
|
||||||
|
|
||||||
|
docs: {
|
||||||
|
autodocs: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
||||||
|
function getAbsolutePath(value) {
|
||||||
|
return dirname(require.resolve(join(value, "package.json")));
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { addDecorator } from "@storybook/react";
|
|
||||||
import { AppRouterContext } from "next/dist/shared/lib/app-router-context";
|
|
||||||
import { I18nextProvider } from "react-i18next";
|
|
||||||
|
|
||||||
import "../styles/globals.css";
|
|
||||||
import "../styles/storybook-styles.css";
|
|
||||||
import i18n from "./i18next";
|
|
||||||
|
|
||||||
export const parameters = {
|
|
||||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nextRouter: {
|
|
||||||
pathname: "/",
|
|
||||||
asPath: "/",
|
|
||||||
query: {},
|
|
||||||
push() {},
|
|
||||||
Provider: AppRouterContext.Provider,
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
locale: "en",
|
|
||||||
locales: {
|
|
||||||
en: "English",
|
|
||||||
fr: "Français",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
i18n,
|
|
||||||
};
|
|
||||||
|
|
||||||
addDecorator((storyFn) => (
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<div style={{ margin: "2rem" }}>{storyFn()}</div>
|
|
||||||
</I18nextProvider>
|
|
||||||
));
|
|
||||||
|
|
||||||
window.getEmbedNamespace = () => {
|
|
||||||
const url = new URL(document.URL);
|
|
||||||
const namespace = url.searchParams.get("embed");
|
|
||||||
return namespace;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.getEmbedTheme = () => {
|
|
||||||
return "auto";
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// adds tooltip context to all stories
|
||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
|
import type { Preview } from "@storybook/react";
|
||||||
|
import React from "react";
|
||||||
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
|
||||||
|
import type { EmbedThemeConfig } from "@calcom/embed-core/src/types";
|
||||||
|
// adds trpc context to all stories (esp. booker)
|
||||||
|
import { StorybookTrpcProvider } from "@calcom/ui";
|
||||||
|
|
||||||
|
import "../styles/globals.css";
|
||||||
|
import "../styles/storybook-styles.css";
|
||||||
|
import i18n from "./i18next";
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
|
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
locale: "en",
|
||||||
|
locales: {
|
||||||
|
en: "English",
|
||||||
|
fr: "Français",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
i18n,
|
||||||
|
|
||||||
|
nextjs: {
|
||||||
|
appDirectory: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<StorybookTrpcProvider>
|
||||||
|
<TooltipProvider>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<div style={{ margin: "2rem" }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
</I18nextProvider>
|
||||||
|
</TooltipProvider>
|
||||||
|
</StorybookTrpcProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
getEmbedNamespace: () => string | null;
|
||||||
|
getEmbedTheme: () => EmbedThemeConfig | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.getEmbedNamespace = () => {
|
||||||
|
const url = new URL(document.URL);
|
||||||
|
const namespace = url.searchParams.get("embed");
|
||||||
|
return namespace;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getEmbedTheme = () => {
|
||||||
|
return "auto";
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ArgsTable } from "@storybook/addon-docs";
|
import { ArgsTable } from "@storybook/addon-docs";
|
||||||
import { SortType } from "@storybook/components";
|
import type { SortType } from "@storybook/blocks";
|
||||||
import { PropDescriptor } from "@storybook/store";
|
import type { PropDescriptor } from "@storybook/preview-api";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore storybook addon types component as any so we have to do
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore storybook addon types component as any so we have to do
|
||||||
type Component = any;
|
type Component = any;
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import { Meta } from "@storybook/addon-docs";
|
|||||||
library, we will be adding more components as we go along.
|
library, we will be adding more components as we go along.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Our{" "}
|
Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a>
|
||||||
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
|
|
||||||
Figma
|
|
||||||
</a>{" "}
|
|
||||||
library is available for anyone to view and use. If you have any questions or concerns, please reach out to
|
library is available for anyone to view and use. If you have any questions or concerns, please reach out to
|
||||||
the design team.
|
the design team.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+16
-15
@@ -3,8 +3,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "start-storybook -p 6006",
|
"dev": "storybook dev -p 6006",
|
||||||
"build": "build-storybook"
|
"build": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "*",
|
||||||
@@ -20,23 +20,25 @@
|
|||||||
"@radix-ui/react-slider": "^1.0.0",
|
"@radix-ui/react-slider": "^1.0.0",
|
||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
|
"@storybook/addon-docs": "^7.6.3",
|
||||||
|
"@storybook/blocks": "^7.6.3",
|
||||||
|
"@storybook/nextjs": "^7.6.3",
|
||||||
|
"@storybook/preview-api": "^7.6.3",
|
||||||
"next": "^13.4.6",
|
"next": "^13.4.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"storybook-addon-next-router": "^4.0.2",
|
|
||||||
"storybook-addon-rtl-direction": "^0.0.19"
|
"storybook-addon-rtl-direction": "^0.0.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.19.6",
|
||||||
"@storybook/addon-actions": "^6.5.13",
|
"@storybook/addon-actions": "^7.6.3",
|
||||||
"@storybook/addon-essentials": "^6.5.13",
|
"@storybook/addon-designs": "^7.0.7",
|
||||||
"@storybook/addon-interactions": "^6.5.13",
|
"@storybook/addon-essentials": "^7.6.3",
|
||||||
"@storybook/addon-links": "^6.5.13",
|
"@storybook/addon-interactions": "^7.6.3",
|
||||||
"@storybook/builder-vite": "^0.2.4",
|
"@storybook/addon-links": "^7.6.3",
|
||||||
"@storybook/builder-webpack5": "^6.5.13",
|
"@storybook/nextjs": "^7.6.3",
|
||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/react": "^7.6.3",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/testing-library": "^0.2.2",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
"@vitejs/plugin-react": "^2.2.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
@@ -46,9 +48,8 @@
|
|||||||
"postcss": "^8.4.18",
|
"postcss": "^8.4.18",
|
||||||
"postcss-pseudo-companion-classes": "^0.1.1",
|
"postcss-pseudo-companion-classes": "^0.1.1",
|
||||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||||
"storybook-addon-designs": "^6.3.1",
|
"storybook": "^7.6.3",
|
||||||
"storybook-addon-next": "^1.6.9",
|
"storybook-react-i18next": "^2.0.9",
|
||||||
"storybook-react-i18next": "^1.1.2",
|
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^4.1.2"
|
"vite": "^4.1.2"
|
||||||
|
|||||||
@@ -197,88 +197,91 @@
|
|||||||
@layer {
|
@layer {
|
||||||
:root {
|
:root {
|
||||||
/* background */
|
/* background */
|
||||||
|
|
||||||
--cal-bg-emphasis: #e5e7eb;
|
--cal-bg-emphasis: hsla(220,13%,91%,1);
|
||||||
--cal-bg: white;
|
--cal-bg: hsla(0,0%,100%,1);
|
||||||
--cal-bg-subtle: #f3f4f6;
|
--cal-bg-subtle: hsla(220, 14%, 96%,1);
|
||||||
--cal-bg-muted: #f9fafb;
|
--cal-bg-muted: hsla(210,20%,98%,1);
|
||||||
--cal-bg-inverted: #111827;
|
--cal-bg-inverted: hsla(0,0%,6%,1);
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #dee9fc;
|
--cal-bg-info: hsla(218,83%,98%,1);
|
||||||
--cal-bg-success: #e2fbe8;
|
--cal-bg-success: hsla(134,76%,94%,1);
|
||||||
--cal-bg-attention: #fceed8;
|
--cal-bg-attention: hsla(37, 86%, 92%, 1);
|
||||||
--cal-bg-error: #f9e3e2;
|
--cal-bg-error: hsla(3,66,93,1);
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--cal-border-emphasis: #9ca3af;
|
--cal-border-emphasis: hsla(218, 11%, 65%, 1);
|
||||||
--cal-border: #d1d5db;
|
--cal-border: hsla(216, 12%, 84%, 1);
|
||||||
--cal-border-subtle: #e5e7eb;
|
--cal-border-subtle: hsla(220, 13%, 91%, 1);
|
||||||
--cal-border-muted: #f3f4f6;
|
--cal-border-booker: #e5e7eb;
|
||||||
--cal-border-error: #aa2e26;
|
--cal-border-muted: hsla(220, 14%, 96%, 1);
|
||||||
|
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #111827;
|
--cal-text-emphasis: hsla(217, 19%, 27%, 1);
|
||||||
--cal-text: #374151;
|
--cal-text: hsla(217, 19%, 27%, 1);
|
||||||
--cal-text-subtle: #6b7280;
|
--cal-text-subtle: hsla(220, 9%, 46%, 1);
|
||||||
--cal-text-muted: #9ca3af;
|
--cal-text-muted: hsla(218, 11%, 65%, 1);
|
||||||
--cal-text-inverted: white;
|
--cal-text-inverted: hsla(0, 0%, 100%, 1);
|
||||||
|
|
||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info: #253985;
|
--cal-text-info: hsla(228, 56%, 33%, 1);
|
||||||
--cal-text-success: #285231;
|
--cal-text-success: hsla(133, 34%, 24%, 1);
|
||||||
--cal-text-attention: #73321b;
|
--cal-text-attention: hsla(16, 62%, 28%, 1);
|
||||||
--cal-text-error: #752522;
|
--cal-text-error: hsla(2, 55%, 30%, 1);
|
||||||
|
|
||||||
/* Brand shinanigans
|
/* Brand shinanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
*/
|
*/
|
||||||
--cal-brand: #111827;
|
--cal-brand: hsla(221, 39%, 11%, 1);
|
||||||
--cal-brand-emphasis: #101010;
|
--cal-brand-emphasis: hsla(0, 0%, 6%, 1);
|
||||||
--cal-brand-text: white;
|
--cal-brand-text: hsla(0, 0%, 100%, 1);
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
/* background */
|
/* background */
|
||||||
|
|
||||||
--cal-bg-emphasis: #2b2b2b;
|
--cal-bg-emphasis: hsla(0, 0%, 32%, 1);
|
||||||
--cal-bg: #101010;
|
--cal-bg: hsla(0, 0%, 10%, 1);
|
||||||
--cal-bg-subtle: #2b2b2b;
|
--cal-bg-subtle: hsla(0, 0%, 18%, 1);
|
||||||
--cal-bg-muted: #1c1c1c;
|
--cal-bg-muted: hsla(0, 0%, 12%, 1);
|
||||||
--cal-bg-inverted: #f3f4f6;
|
--cal-bg-inverted: hsla(220, 14%, 96%, 1);
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #263fa9;
|
--cal-bg-info: hsla(228, 56%, 33%, 1);
|
||||||
--cal-bg-success: #306339;
|
--cal-bg-success: hsla(133, 34%, 24%, 1);
|
||||||
--cal-bg-attention: #8e3b1f;
|
--cal-bg-attention: hsla(16, 62%, 28%, 1);
|
||||||
--cal-bg-error: #8c2822;
|
--cal-bg-error: hsla(2, 55%, 30%, 1);
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--cal-border-emphasis: #575757;
|
--cal-border-emphasis: hsla(0, 0%, 46%, 1);
|
||||||
--cal-border: #444444;
|
--cal-border: hsla(0, 0%, 34%, 1);
|
||||||
--cal-border-subtle: #2b2b2b;
|
--cal-border-subtle: hsla(0, 0%, 22%, 1);
|
||||||
--cal-border-muted: #1c1c1c;
|
--cal-border-booker: hsla(0, 0%, 22%, 1);
|
||||||
--cal-border-error: #aa2e26;
|
--cal-border-muted: hsla(0, 0%, 18%, 1);
|
||||||
|
--cal-border-error: hsla(4, 63%, 41%, 1);
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #f3f4f6;
|
--cal-text-emphasis: hsla(240, 20%, 99%, 1);
|
||||||
--cal-text: #d6d6d6;
|
--cal-text: hsla(0, 0%, 84%, 1);
|
||||||
--cal-text-subtle: #757575;
|
--cal-text-subtle: hsla(0, 0%, 65%, 1);
|
||||||
--cal-text-muted: #575757;
|
--cal-text-muted: hsla(0, 0%, 34%, 1);
|
||||||
--cal-text-inverted: #101010;
|
--cal-text-inverted: hsla(0, 0%, 10%, 1);
|
||||||
|
|
||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info: #dee9fc;
|
--cal-text-info: hsla(218, 83%, 93%, 1);
|
||||||
--cal-text-success: #e2fbe8;
|
--cal-text-success: hsla(134, 76%, 94%, 1);
|
||||||
--cal-text-attention: #fceed8;
|
--cal-text-attention: hsla(37, 86%, 92%, 1);
|
||||||
--cal-text-error: #f9e3e2;
|
--cal-text-error: hsla(3, 66%, 93%, 1);
|
||||||
|
|
||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
*/
|
*/
|
||||||
--cal-brand: white;
|
--cal-brand: hsla(0, 0%, 100%, 1);
|
||||||
--cal-brand-emphasis: #e1e1e1;
|
--cal-brand-emphasis: hsla(218, 11%, 65%, 1);
|
||||||
--cal-brand-text: black;
|
--cal-brand-text: hsla(0, 0%, 0%,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createHydrateClient } from "app/_trpc/createHydrateClient";
|
import { createHydrateClient } from "_app/_trpc/createHydrateClient";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
export const HydrateClient = createHydrateClient({
|
export const HydrateClient = createHydrateClient({
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,212 @@
|
|||||||
|
// originally from in the "experimental playground for tRPC + next.js 13" repo owned by trpc team
|
||||||
|
// file link: https://github.com/trpc/next-13/blob/main/%40trpc/next-layout/createTRPCNextLayout.ts
|
||||||
|
// repo link: https://github.com/trpc/next-13
|
||||||
|
// code is / will continue to be adapted for our usage
|
||||||
|
import { dehydrate, QueryClient } from "@tanstack/query-core";
|
||||||
|
import type { DehydratedState, QueryKey } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type { Maybe, TRPCClientError, TRPCClientErrorLike } from "@calcom/trpc";
|
||||||
|
import {
|
||||||
|
callProcedure,
|
||||||
|
type AnyProcedure,
|
||||||
|
type AnyQueryProcedure,
|
||||||
|
type AnyRouter,
|
||||||
|
type DataTransformer,
|
||||||
|
type inferProcedureInput,
|
||||||
|
type inferProcedureOutput,
|
||||||
|
type inferRouterContext,
|
||||||
|
type MaybePromise,
|
||||||
|
type ProcedureRouterRecord,
|
||||||
|
} from "@calcom/trpc/server";
|
||||||
|
|
||||||
|
import { createRecursiveProxy, createFlatProxy } from "@trpc/server/shared";
|
||||||
|
|
||||||
|
export function getArrayQueryKey(
|
||||||
|
queryKey: string | [string] | [string, ...unknown[]] | unknown[],
|
||||||
|
type: string
|
||||||
|
): QueryKey {
|
||||||
|
const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey];
|
||||||
|
const [arrayPath, input] = queryKeyArrayed;
|
||||||
|
|
||||||
|
if (!input && (!type || type === "any")) {
|
||||||
|
return Array.isArray(arrayPath) && arrayPath.length !== 0 ? [arrayPath] : ([] as unknown as QueryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
arrayPath,
|
||||||
|
{
|
||||||
|
...(typeof input !== "undefined" && { input: input }),
|
||||||
|
...(type && type !== "any" && { type: type }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy starts
|
||||||
|
// copied from trpc/trpc repo
|
||||||
|
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L37-#L58
|
||||||
|
function transformQueryOrMutationCacheErrors<
|
||||||
|
TState extends DehydratedState["queries"][0] | DehydratedState["mutations"][0]
|
||||||
|
>(result: TState): TState {
|
||||||
|
const error = result.state.error as Maybe<TRPCClientError<any>>;
|
||||||
|
if (error instanceof Error && error.name === "TRPCClientError") {
|
||||||
|
const newError: TRPCClientErrorLike<any> = {
|
||||||
|
message: error.message,
|
||||||
|
data: error.data,
|
||||||
|
shape: error.shape,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
state: {
|
||||||
|
...result.state,
|
||||||
|
error: newError,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// copy ends
|
||||||
|
|
||||||
|
interface CreateTRPCNextLayoutOptions<TRouter extends AnyRouter> {
|
||||||
|
router: TRouter;
|
||||||
|
createContext: () => MaybePromise<inferRouterContext<TRouter>>;
|
||||||
|
transformer?: DataTransformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type DecorateProcedure<TProcedure extends AnyProcedure> = TProcedure extends AnyQueryProcedure
|
||||||
|
? {
|
||||||
|
fetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||||
|
fetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||||
|
prefetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||||
|
prefetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||||
|
}
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type OmitNever<TType> = Pick<
|
||||||
|
TType,
|
||||||
|
{
|
||||||
|
[K in keyof TType]: TType[K] extends never ? never : K;
|
||||||
|
}[keyof TType]
|
||||||
|
>;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type DecoratedProcedureRecord<
|
||||||
|
TProcedures extends ProcedureRouterRecord,
|
||||||
|
TPath extends string = ""
|
||||||
|
> = OmitNever<{
|
||||||
|
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
|
||||||
|
? DecoratedProcedureRecord<TProcedures[TKey]["_def"]["record"], `${TPath}${TKey & string}.`>
|
||||||
|
: TProcedures[TKey] extends AnyQueryProcedure
|
||||||
|
? DecorateProcedure<TProcedures[TKey]>
|
||||||
|
: never;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type CreateTRPCNextLayout<TRouter extends AnyRouter> = DecoratedProcedureRecord<TRouter["_def"]["record"]> & {
|
||||||
|
dehydrate(): Promise<DehydratedState>;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStateContainer = <TRouter extends AnyRouter>(opts: CreateTRPCNextLayoutOptions<TRouter>) => {
|
||||||
|
let _trpc: {
|
||||||
|
queryClient: QueryClient;
|
||||||
|
context: inferRouterContext<TRouter>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (_trpc === null) {
|
||||||
|
_trpc = {
|
||||||
|
context: opts.createContext(),
|
||||||
|
queryClient: new QueryClient(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return _trpc;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createTRPCNextLayout<TRouter extends AnyRouter>(
|
||||||
|
opts: CreateTRPCNextLayoutOptions<TRouter>
|
||||||
|
): CreateTRPCNextLayout<TRouter> {
|
||||||
|
const getState = getStateContainer(opts);
|
||||||
|
|
||||||
|
const transformer = opts.transformer ?? {
|
||||||
|
serialize: (v) => v,
|
||||||
|
deserialize: (v) => v,
|
||||||
|
};
|
||||||
|
|
||||||
|
return createFlatProxy((key) => {
|
||||||
|
const state = getState();
|
||||||
|
const { queryClient } = state;
|
||||||
|
if (key === "queryClient") {
|
||||||
|
return queryClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "dehydrate") {
|
||||||
|
// copy starts
|
||||||
|
// copied from trpc/trpc repo
|
||||||
|
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L214-#L229
|
||||||
|
const dehydratedCache = dehydrate(queryClient, {
|
||||||
|
shouldDehydrateQuery() {
|
||||||
|
// makes sure errors are also dehydrated
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects
|
||||||
|
const dehydratedCacheWithErrors = {
|
||||||
|
...dehydratedCache,
|
||||||
|
queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors),
|
||||||
|
mutations: dehydratedCache.mutations.map(transformQueryOrMutationCacheErrors),
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => transformer.serialize(dehydratedCacheWithErrors);
|
||||||
|
}
|
||||||
|
// copy ends
|
||||||
|
|
||||||
|
return createRecursiveProxy(async (callOpts) => {
|
||||||
|
const path = [key, ...callOpts.path];
|
||||||
|
const utilName = path.pop();
|
||||||
|
const ctx = await state.context;
|
||||||
|
|
||||||
|
const caller = opts.router.createCaller(ctx);
|
||||||
|
|
||||||
|
const pathStr = path.join(".");
|
||||||
|
const input = callOpts.args[0];
|
||||||
|
|
||||||
|
if (utilName === "fetchInfinite") {
|
||||||
|
return queryClient.fetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
|
||||||
|
caller.query(pathStr, input)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utilName === "prefetch") {
|
||||||
|
return queryClient.prefetchQuery({
|
||||||
|
queryKey: getArrayQueryKey([path, input], "query"),
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await callProcedure({
|
||||||
|
procedures: opts.router._def.procedures,
|
||||||
|
path: pathStr,
|
||||||
|
rawInput: input,
|
||||||
|
ctx,
|
||||||
|
type: "query",
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utilName === "prefetchInfinite") {
|
||||||
|
return queryClient.prefetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
|
||||||
|
caller.query(pathStr, input)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryClient.fetchQuery(getArrayQueryKey([path, input], "query"), () =>
|
||||||
|
caller.query(pathStr, input)
|
||||||
|
);
|
||||||
|
}) as CreateTRPCNextLayout<TRouter>;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { TRPCContext } from "@calcom/trpc/server/createContext";
|
||||||
|
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
|
export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||||
|
import prisma, { readonlyPrisma } from "@calcom/prisma";
|
||||||
|
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
|
import { createTRPCNextLayout } from "./createTRPCNextLayout";
|
||||||
|
|
||||||
|
export async function ssgInit() {
|
||||||
|
const locale = headers().get("x-locale") ?? "en";
|
||||||
|
|
||||||
|
const i18n = (await serverSideTranslations(locale, ["common"])) || "en";
|
||||||
|
|
||||||
|
const ssg = createTRPCNextLayout({
|
||||||
|
router: appRouter,
|
||||||
|
transformer: superjson,
|
||||||
|
createContext() {
|
||||||
|
return { prisma, insightsDb: readonlyPrisma, session: null, locale, i18n };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
|
||||||
|
// we can set query data directly to the queryClient
|
||||||
|
const queryKey = [
|
||||||
|
["viewer", "public", "i18n"],
|
||||||
|
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
|
||||||
|
];
|
||||||
|
|
||||||
|
ssg.queryClient.setQueryData(queryKey, { i18n });
|
||||||
|
|
||||||
|
return ssg;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { type GetServerSidePropsContext } from "next";
|
||||||
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
import { headers, cookies } from "next/headers";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||||
|
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||||
|
import prisma, { readonlyPrisma } from "@calcom/prisma";
|
||||||
|
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
|
import { createTRPCNextLayout } from "./createTRPCNextLayout";
|
||||||
|
|
||||||
|
export async function ssrInit(options?: { noI18nPreload: boolean }) {
|
||||||
|
const req = {
|
||||||
|
headers: headers(),
|
||||||
|
cookies: cookies(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const locale = await getLocale(req);
|
||||||
|
|
||||||
|
const i18n = (await serverSideTranslations(locale, ["common", "vital"])) || "en";
|
||||||
|
|
||||||
|
const ssr = createTRPCNextLayout({
|
||||||
|
router: appRouter,
|
||||||
|
transformer: superjson,
|
||||||
|
createContext() {
|
||||||
|
return {
|
||||||
|
prisma,
|
||||||
|
insightsDb: readonlyPrisma,
|
||||||
|
session: null,
|
||||||
|
locale,
|
||||||
|
i18n,
|
||||||
|
req: req as unknown as GetServerSidePropsContext["req"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
|
||||||
|
// we can set query data directly to the queryClient
|
||||||
|
const queryKey = [
|
||||||
|
["viewer", "public", "i18n"],
|
||||||
|
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
|
||||||
|
];
|
||||||
|
if (!options?.noI18nPreload) {
|
||||||
|
ssr.queryClient.setQueryData(queryKey, { i18n });
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled([
|
||||||
|
// So feature flags are available on first render
|
||||||
|
ssr.viewer.features.map.prefetch(),
|
||||||
|
// Provides a better UX to the users who have already upgraded.
|
||||||
|
ssr.viewer.teams.hasTeamPlan.prefetch(),
|
||||||
|
ssr.viewer.public.session.prefetch(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ssr;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { HydrateClient } from "app/_trpc/HydrateClient";
|
import { HydrateClient } from "_app/_trpc/HydrateClient";
|
||||||
import { trpc } from "app/_trpc/client";
|
import { trpc } from "_app/_trpc/client";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
@@ -8,33 +8,8 @@ import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
|||||||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
||||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
||||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
||||||
|
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
||||||
|
|
||||||
const ENDPOINTS = [
|
|
||||||
"admin",
|
|
||||||
"apiKeys",
|
|
||||||
"appRoutingForms",
|
|
||||||
"apps",
|
|
||||||
"auth",
|
|
||||||
"availability",
|
|
||||||
"appBasecamp3",
|
|
||||||
"bookings",
|
|
||||||
"deploymentSetup",
|
|
||||||
"eventTypes",
|
|
||||||
"features",
|
|
||||||
"insights",
|
|
||||||
"payments",
|
|
||||||
"public",
|
|
||||||
"saml",
|
|
||||||
"slots",
|
|
||||||
"teams",
|
|
||||||
"organizations",
|
|
||||||
"users",
|
|
||||||
"viewer",
|
|
||||||
"webhook",
|
|
||||||
"workflows",
|
|
||||||
"appsRouter",
|
|
||||||
"googleWorkspace",
|
|
||||||
] as const;
|
|
||||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type Params = {
|
||||||
|
[param: string]: string | string[] | undefined;
|
||||||
|
};
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,15 @@
|
|||||||
|
import { type ReactElement } from "react";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
|
type EventTypesLayoutProps = {
|
||||||
|
children: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Layout({ children }: EventTypesLayoutProps) {
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import AppPage from "@pages/apps/[slug]/index";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
import fs from "fs";
|
||||||
|
import matter from "gray-matter";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import path from "path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||||
|
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
|
||||||
|
import { APP_NAME, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
const sourceSchema = z.object({
|
||||||
|
content: z.string(),
|
||||||
|
data: z.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
items: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
iframe: z.object({ src: z.string() }),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
const { data } = await getPageProps({ params });
|
||||||
|
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => `${data.name} | ${APP_NAME}`,
|
||||||
|
() => data.description
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateStaticParams = async () => {
|
||||||
|
try {
|
||||||
|
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||||
|
return appStore.map(({ slug }) => ({ slug }));
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||||
|
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
if (typeof params?.slug !== "string") {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const appMeta = await getAppWithMetadata({
|
||||||
|
slug: params?.slug,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appFromDb = await prisma.app.findUnique({
|
||||||
|
where: { slug: params.slug.toLowerCase() },
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAppAvailableInFileSystem = appMeta;
|
||||||
|
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||||
|
|
||||||
|
if (!IS_PRODUCTION && isAppDisabled) {
|
||||||
|
return {
|
||||||
|
isAppDisabled: true as const,
|
||||||
|
data: {
|
||||||
|
...appMeta,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appFromDb || !appMeta || isAppDisabled) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTemplate = appMeta.isTemplate;
|
||||||
|
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||||
|
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||||
|
const postFilePath = path.join(README_PATH);
|
||||||
|
let source = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
source = fs.readFileSync(postFilePath).toString();
|
||||||
|
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||||
|
} catch (error) {
|
||||||
|
/* If the app doesn't have a README we fallback to the package description */
|
||||||
|
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||||
|
source = appMeta.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = matter(source);
|
||||||
|
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||||
|
if (data.items) {
|
||||||
|
data.items = data.items.map((item) => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return getAppAssetFullPath(item, {
|
||||||
|
dirName: appMeta.dirName,
|
||||||
|
isTemplate: appMeta.isTemplate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isAppDisabled: false as const,
|
||||||
|
source: { content, data },
|
||||||
|
data: appMeta,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||||
|
const pageProps = await getPageProps({ params });
|
||||||
|
|
||||||
|
return <AppPage {...pageProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import SetupPage from "@pages/apps/[slug]/setup";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => `${params.slug} | ${APP_NAME}`,
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
const req = { headers: headers(), cookies: cookies() };
|
||||||
|
|
||||||
|
const result = await getServerSideProps({ params, req } as unknown as GetServerSidePropsContext);
|
||||||
|
|
||||||
|
if (!result || "notFound" in result) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("redirect" in result) {
|
||||||
|
redirect(result.redirect.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.props;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||||
|
const pageProps = await getPageProps({ params });
|
||||||
|
return <SetupPage {...pageProps} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import CategoryPage from "@pages/apps/categories/[category]";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => `${APP_NAME} | ${APP_NAME}`,
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateStaticParams = async () => {
|
||||||
|
const paths = Object.keys(AppCategories);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.$queryRaw`SELECT 1`;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||||
|
// Database is not available at build time. Make sure we fall back to building these pages on demand
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths.map((category) => ({ category }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
category: z.nativeEnum(AppCategories),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
const p = querySchema.safeParse(params);
|
||||||
|
|
||||||
|
if (!p.success) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const appQuery = await prisma.app.findMany({
|
||||||
|
where: {
|
||||||
|
categories: {
|
||||||
|
has: p.data.category,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dbAppsSlugs = appQuery.map((category) => category.slug);
|
||||||
|
|
||||||
|
const appStore = await getAppRegistry();
|
||||||
|
|
||||||
|
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
|
||||||
|
return {
|
||||||
|
apps,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||||
|
const { apps } = await getPageProps({ params });
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||||
|
<CategoryPage apps={apps} />
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import LegacyPage from "@pages/apps/categories/index";
|
||||||
|
import { ssrInit } from "_app/_trpc/ssrInit";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
|
||||||
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => `Categories | ${APP_NAME}`,
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getPageProps() {
|
||||||
|
const ssr = await ssrInit();
|
||||||
|
const req = { headers: headers(), cookies: cookies() };
|
||||||
|
|
||||||
|
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage
|
||||||
|
const session = await getServerSession({ req });
|
||||||
|
|
||||||
|
let appStore;
|
||||||
|
if (session?.user?.id) {
|
||||||
|
appStore = await getAppRegistryWithCredentials(session.user.id);
|
||||||
|
} else {
|
||||||
|
appStore = await getAppRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = appStore.reduce((c, app) => {
|
||||||
|
for (const category of app.categories) {
|
||||||
|
c[category] = c[category] ? c[category] + 1 : 1;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
return {
|
||||||
|
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
|
||||||
|
dehydratedState: await ssr.dehydrate(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const props = await getPageProps();
|
||||||
|
const h = headers();
|
||||||
|
const nonce = h.get("x-nonce") ?? undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
|
||||||
|
<LegacyPage {...props} />
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { type ReactElement } from "react";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
|
type EventTypesLayoutProps = {
|
||||||
|
children: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Layout({ children }: EventTypesLayoutProps) {
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import LegacyPage from "@pages/apps/installed/[category]";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
category: z.nativeEnum(AppCategories),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
return await _generateMetadata(
|
||||||
|
(t) => `${t("installed_apps")} | ${APP_NAME}`,
|
||||||
|
(t) => t("manage_your_connected_apps")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
const p = querySchema.safeParse(params);
|
||||||
|
|
||||||
|
if (!p.success) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: p.data.category,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||||
|
const { category } = await getPageProps({ params });
|
||||||
|
|
||||||
|
return <LegacyPage />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
|
import { type ReactElement } from "react";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
|
||||||
|
|
||||||
|
type WrapperWithLayoutProps = {
|
||||||
|
children: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
|
||||||
|
const h = headers();
|
||||||
|
const nonce = h.get("x-nonce") ?? undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import Page from "@pages/settings/admin/apps/[category]";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("apps"),
|
||||||
|
(t) => t("admin_apps_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import Page from "@pages/settings/admin/apps/index";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("apps"),
|
||||||
|
(t) => t("admin_apps_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import Page from "@pages/settings/admin/flags";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "Feature Flags",
|
||||||
|
() => "Here you can toggle your Cal.com instance features."
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import Page from "@pages/settings/admin/impersonation";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("admin"),
|
||||||
|
(t) => t("impersonation")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import Page from "@pages/settings/admin/oAuth/index";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "OAuth",
|
||||||
|
() => "Add new OAuth Clients"
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Page from "@pages/settings/admin/index";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "Admin",
|
||||||
|
() => "admin_description"
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import EventTypes from "@pages/event-types";
|
import EventTypes from "@pages/event-types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
export const generateMetadata = async () =>
|
export const generateMetadata = async () =>
|
||||||
await _generateMetadata(
|
await _generateMetadata(
|
||||||
File renamed without changes.
@@ -0,0 +1,20 @@
|
|||||||
|
// pages containing layout (e.g., /availability/[schedule].tsx) are supposed to go under (no-layout) folder
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { type ReactElement } from "react";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
|
type WrapperWithoutLayoutProps = {
|
||||||
|
children: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function WrapperWithoutLayout({ children }: WrapperWithoutLayoutProps) {
|
||||||
|
const h = headers();
|
||||||
|
const nonce = h.get("x-nonce") ?? undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import Page from "@pages/settings/admin/oAuth/oAuthView";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "OAuth",
|
||||||
|
() => "Add new OAuth Clients"
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
|
import { type ReactElement } from "react";
|
||||||
|
|
||||||
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
|
type WrapperWithLayoutProps = {
|
||||||
|
children: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
|
||||||
|
const h = headers();
|
||||||
|
const nonce = h.get("x-nonce") ?? undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
import Page from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
(t) => t("organizations"),
|
||||||
|
(t) => t("orgs_page_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { getServerCaller } from "_app/_trpc/serverClient";
|
||||||
|
import { type Params } from "_app/_types";
|
||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import Page from "@calcom/features/ee/users/pages/users-edit-view";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
const userIdSchema = z.object({ id: z.coerce.number() });
|
||||||
|
|
||||||
|
export const generateMetadata = async ({ params }: { params: Params }) => {
|
||||||
|
const input = userIdSchema.safeParse(params);
|
||||||
|
|
||||||
|
let title = "";
|
||||||
|
if (!input.success) {
|
||||||
|
title = "Editing user";
|
||||||
|
} else {
|
||||||
|
const req = {
|
||||||
|
headers: headers(),
|
||||||
|
cookies: cookies(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest'
|
||||||
|
const data = await getServerCaller({ req, prisma }).viewer.users.get({ userId: input.data.id });
|
||||||
|
const { user } = data;
|
||||||
|
title = `Editing user: ${user.username}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => title,
|
||||||
|
() => "Here you can edit a current user."
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
import Page from "@calcom/features/ee/users/pages/users-add-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "Add new user",
|
||||||
|
() => "Here you can add a new user."
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { _generateMetadata } from "_app/_utils";
|
||||||
|
|
||||||
|
import Page from "@calcom/features/ee/users/pages/users-listing-view";
|
||||||
|
|
||||||
|
export const generateMetadata = async () =>
|
||||||
|
await _generateMetadata(
|
||||||
|
() => "Users",
|
||||||
|
() => "A list of all the users in your account including their name, title, email and role."
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
File renamed without changes.
@@ -1,4 +1,6 @@
|
|||||||
import { dir } from "i18next";
|
import { dir } from "i18next";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
import { headers, cookies } from "next/headers";
|
import { headers, cookies } from "next/headers";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -10,6 +12,14 @@ import { prepareRootMetadata } from "@lib/metadata";
|
|||||||
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
|
||||||
|
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||||
|
const calFont = localFont({
|
||||||
|
src: "../fonts/CalSans-SemiBold.woff2",
|
||||||
|
variable: "--font-cal",
|
||||||
|
preload: true,
|
||||||
|
display: "block",
|
||||||
|
});
|
||||||
|
|
||||||
export const generateMetadata = () =>
|
export const generateMetadata = () =>
|
||||||
prepareRootMetadata({
|
prepareRootMetadata({
|
||||||
twitterCreator: "@calcom",
|
twitterCreator: "@calcom",
|
||||||
@@ -66,6 +76,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||||||
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
|
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<style>{`
|
||||||
|
:root {
|
||||||
|
--font-inter: ${interFont.style.fontFamily.replace(/\'/g, "")};
|
||||||
|
--font-cal: ${calFont.style.fontFamily.replace(/\'/g, "")};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
|
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
|
||||||
@@ -5,6 +5,12 @@ import z from "zod";
|
|||||||
|
|
||||||
const ROUTES: [URLPattern, boolean][] = [
|
const ROUTES: [URLPattern, boolean][] = [
|
||||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||||
|
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||||
|
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||||
|
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||||
|
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||||
|
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
||||||
|
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
||||||
].map(([pathname, enabled]) => [
|
].map(([pathname, enabled]) => [
|
||||||
new URLPattern({
|
new URLPattern({
|
||||||
pathname,
|
pathname,
|
||||||
@@ -27,7 +33,6 @@ export const abTestMiddlewareFactory =
|
|||||||
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
||||||
|
|
||||||
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
||||||
|
|
||||||
const enabled = route !== null ? route[1] || override : false;
|
const enabled = route !== null ? route[1] || override : false;
|
||||||
|
|
||||||
if (pathname.includes("future") || !enabled) {
|
if (pathname.includes("future") || !enabled) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type AppListCardProps = {
|
|||||||
invalidCredential?: boolean;
|
invalidCredential?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
credentialOwner?: CredentialOwner;
|
credentialOwner?: CredentialOwner;
|
||||||
|
className?: string;
|
||||||
} & ShouldHighlight;
|
} & ShouldHighlight;
|
||||||
|
|
||||||
const schema = z.object({ hl: z.string().optional() });
|
const schema = z.object({ hl: z.string().optional() });
|
||||||
@@ -50,6 +51,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
invalidCredential,
|
invalidCredential,
|
||||||
children,
|
children,
|
||||||
credentialOwner,
|
credentialOwner,
|
||||||
|
className,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
data: { hl },
|
data: { hl },
|
||||||
@@ -83,7 +85,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
}, [highlight, pathname, router, searchParams, shouldHighlight]);
|
}, [highlight, pathname, router, searchParams, shouldHighlight]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100")}>
|
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100", className)}>
|
||||||
<div className="flex items-center gap-x-3 px-4 py-4 sm:px-6">
|
<div className="flex items-center gap-x-3 px-4 py-4 sm:px-6">
|
||||||
{logo ? (
|
{logo ? (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
@@ -18,7 +17,13 @@ export interface CalPageWrapper {
|
|||||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
const interFont = localFont({
|
||||||
|
src: "../fonts/InterVariable.woff2",
|
||||||
|
variable: "--font-inter",
|
||||||
|
preload: true,
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
const calFont = localFont({
|
const calFont = localFont({
|
||||||
src: "../fonts/CalSans-SemiBold.woff2",
|
src: "../fonts/CalSans-SemiBold.woff2",
|
||||||
variable: "--font-cal",
|
variable: "--font-cal",
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { type DehydratedState } from "@tanstack/react-query";
|
import { type DehydratedState } from "@tanstack/react-query";
|
||||||
import type { SSRConfig } from "next-i18next";
|
import type { SSRConfig } from "next-i18next";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import localFont from "next/font/local";
|
|
||||||
// import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
// import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
@@ -20,14 +18,6 @@ export interface CalPageWrapper {
|
|||||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
|
||||||
const calFont = localFont({
|
|
||||||
src: "../fonts/CalSans-SemiBold.woff2",
|
|
||||||
variable: "--font-cal",
|
|
||||||
preload: true,
|
|
||||||
display: "swap",
|
|
||||||
});
|
|
||||||
|
|
||||||
export type PageWrapperProps = Readonly<{
|
export type PageWrapperProps = Readonly<{
|
||||||
getLayout: ((page: React.ReactElement) => ReactNode) | null;
|
getLayout: ((page: React.ReactElement) => ReactNode) | null;
|
||||||
children: React.ReactElement;
|
children: React.ReactElement;
|
||||||
@@ -71,13 +61,6 @@ function PageWrapper(props: PageWrapperProps) {
|
|||||||
id="page-status"
|
id="page-status"
|
||||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
/>
|
/>
|
||||||
<style jsx global>{`
|
|
||||||
:root {
|
|
||||||
--font-inter: ${interFont.style.fontFamily};
|
|
||||||
--font-cal: ${calFont.style.fontFamily};
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{getLayout(
|
{getLayout(
|
||||||
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : props.children
|
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : props.children
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import {
|
|||||||
List,
|
List,
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
|
Label,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Calendar, Plus } from "@calcom/ui/components/icon";
|
import { Calendar } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ type Props = {
|
|||||||
onChanged: () => unknown | Promise<unknown>;
|
onChanged: () => unknown | Promise<unknown>;
|
||||||
fromOnboarding?: boolean;
|
fromOnboarding?: boolean;
|
||||||
destinationCalendarId?: string;
|
destinationCalendarId?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CalendarList(props: Props) {
|
function CalendarList(props: Props) {
|
||||||
@@ -70,8 +72,9 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
||||||
suspense: true,
|
suspense: true,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
const { fromOnboarding } = props;
|
const { fromOnboarding, isLoading } = props;
|
||||||
return (
|
return (
|
||||||
<QueryCell
|
<QueryCell
|
||||||
query={query}
|
query={query}
|
||||||
@@ -82,74 +85,94 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<div className="border-subtle mt-6 rounded-lg border">
|
||||||
{data.connectedCalendars.map((item) => (
|
<div className="border-subtle border-b p-6">
|
||||||
<Fragment key={item.credentialId}>
|
<div className="flex items-center justify-between">
|
||||||
{item.calendars ? (
|
<div>
|
||||||
<AppListCard
|
<h4 className="text-emphasis text-base font-semibold leading-5">
|
||||||
shouldHighlight
|
{t("check_for_conflicts")}
|
||||||
slug={item.integration.slug}
|
</h4>
|
||||||
title={item.integration.name}
|
<p className="text-default text-sm leading-tight">{t("select_calendars")}</p>
|
||||||
logo={item.integration.logo}
|
</div>
|
||||||
description={item.primary?.email ?? item.integration.description}
|
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
||||||
actions={
|
{!!data.connectedCalendars.length && (
|
||||||
<div className="flex w-32 justify-end">
|
<div className="flex items-center">
|
||||||
<DisconnectIntegration
|
<AdditionalCalendarSelector isLoading={isLoading} />
|
||||||
credentialId={item.credentialId}
|
|
||||||
trashIcon
|
|
||||||
onSuccess={props.onChanged}
|
|
||||||
buttonProps={{ className: "border border-default" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<div className="border-subtle border-t">
|
|
||||||
{!fromOnboarding && (
|
|
||||||
<>
|
|
||||||
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
|
||||||
<ul className="space-y-4 px-5 py-4">
|
|
||||||
{item.calendars.map((cal) => (
|
|
||||||
<CalendarSwitch
|
|
||||||
key={cal.externalId}
|
|
||||||
externalId={cal.externalId}
|
|
||||||
title={cal.name || "Nameless calendar"}
|
|
||||||
name={cal.name || "Nameless calendar"}
|
|
||||||
type={item.integration.type}
|
|
||||||
isChecked={cal.isSelected}
|
|
||||||
destination={cal.externalId === props.destinationCalendarId}
|
|
||||||
credentialId={cal.credentialId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</AppListCard>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<Alert
|
</div>
|
||||||
severity="warning"
|
</div>
|
||||||
title={t("something_went_wrong")}
|
<List noBorderTreatment className="p-6 pt-2">
|
||||||
message={
|
{data.connectedCalendars.map((item) => (
|
||||||
<span>
|
<Fragment key={item.credentialId}>
|
||||||
<Link href={`/apps/${item.integration.slug}`}>{item.integration.name}</Link>:{" "}
|
{item.calendars ? (
|
||||||
{t("calendar_error")}
|
<AppListCard
|
||||||
</span>
|
shouldHighlight
|
||||||
}
|
slug={item.integration.slug}
|
||||||
iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5"
|
title={item.integration.name}
|
||||||
actions={
|
logo={item.integration.logo}
|
||||||
<div className="flex w-32 justify-end md:pr-1">
|
description={item.primary?.email ?? item.integration.description}
|
||||||
<DisconnectIntegration
|
className="border-subtle mt-4 rounded-lg border"
|
||||||
credentialId={item.credentialId}
|
actions={
|
||||||
trashIcon
|
<div className="flex w-32 justify-end">
|
||||||
onSuccess={props.onChanged}
|
<DisconnectIntegration
|
||||||
buttonProps={{ className: "border border-default" }}
|
credentialId={item.credentialId}
|
||||||
/>
|
trashIcon
|
||||||
|
onSuccess={props.onChanged}
|
||||||
|
buttonProps={{ className: "border border-default" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<div className="border-subtle border-t">
|
||||||
|
{!fromOnboarding && (
|
||||||
|
<>
|
||||||
|
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
||||||
|
<ul className="space-y-4 px-5 py-4">
|
||||||
|
{item.calendars.map((cal) => (
|
||||||
|
<CalendarSwitch
|
||||||
|
key={cal.externalId}
|
||||||
|
externalId={cal.externalId}
|
||||||
|
title={cal.name || "Nameless calendar"}
|
||||||
|
name={cal.name || "Nameless calendar"}
|
||||||
|
type={item.integration.type}
|
||||||
|
isChecked={cal.isSelected}
|
||||||
|
destination={cal.externalId === props.destinationCalendarId}
|
||||||
|
credentialId={cal.credentialId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
</AppListCard>
|
||||||
/>
|
) : (
|
||||||
)}
|
<Alert
|
||||||
</Fragment>
|
severity="warning"
|
||||||
))}
|
title={t("something_went_wrong")}
|
||||||
</List>
|
message={
|
||||||
|
<span>
|
||||||
|
<Link href={`/apps/${item.integration.slug}`}>{item.integration.name}</Link>:{" "}
|
||||||
|
{t("calendar_error")}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5"
|
||||||
|
actions={
|
||||||
|
<div className="flex w-32 justify-end md:pr-1">
|
||||||
|
<DisconnectIntegration
|
||||||
|
credentialId={item.credentialId}
|
||||||
|
trashIcon
|
||||||
|
onSuccess={props.onChanged}
|
||||||
|
buttonProps={{ className: "border border-default" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -187,42 +210,28 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||||
<>
|
<>
|
||||||
{heading && (
|
{heading && (
|
||||||
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
|
<>
|
||||||
<ShellSubHeading
|
<div className="border-subtle mb-6 mt-8 rounded-lg border">
|
||||||
title={t("calendar")}
|
<div className="p-6">
|
||||||
subtitle={t("installed_app_calendar_description")}
|
<h2 className="text-emphasis mb-1 text-base font-bold leading-5 tracking-wide">
|
||||||
className="mb-0 flex flex-wrap items-center gap-4 sm:flex-nowrap md:mb-3 md:gap-0"
|
{t("add_to_calendar")}
|
||||||
actions={
|
</h2>
|
||||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
|
||||||
{!!data.connectedCalendars.length && (
|
<p className="text-subtle text-sm leading-tight">
|
||||||
<div className="flex items-center">
|
{t("add_to_calendar_description")}
|
||||||
<AdditionalCalendarSelector isLoading={mutation.isLoading} />
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="border-t">
|
||||||
</div>
|
<div className="border-subtle flex w-full flex-col space-y-3 border-y-0 p-6">
|
||||||
}
|
<div>
|
||||||
/>
|
<Label className="text-default mb-0 font-medium">{t("add_events_to")}</Label>
|
||||||
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
|
<DestinationCalendarSelector
|
||||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
hidePlaceholder
|
||||||
<div className="bg-default border-subtle relative rounded-md border p-1.5">
|
value={data.destinationCalendar?.externalId}
|
||||||
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
|
onChange={mutation.mutate}
|
||||||
<Plus
|
isLoading={mutation.isLoading}
|
||||||
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
|
/>
|
||||||
strokeWidth="4"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="md:w-6/12">
|
|
||||||
<h1 className="text-emphasis text-sm font-semibold">{t("create_events_on")}</h1>
|
|
||||||
<p className="text-default text-sm font-normal">{t("set_calendar")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="justify-end md:w-6/12">
|
|
||||||
<DestinationCalendarSelector
|
|
||||||
onChange={mutation.mutate}
|
|
||||||
hidePlaceholder
|
|
||||||
isLoading={mutation.isLoading}
|
|
||||||
value={data.destinationCalendar?.externalId}
|
|
||||||
hideAdvancedText
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,8 +239,9 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
onChanged={onChanged}
|
onChanged={onChanged}
|
||||||
fromOnboarding={fromOnboarding}
|
fromOnboarding={fromOnboarding}
|
||||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||||
|
isLoading={mutation.isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : fromOnboarding ? (
|
) : fromOnboarding ? (
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const InstallAppButtonChild = ({
|
|||||||
disabled={shouldDisableInstallation}
|
disabled={shouldDisableInstallation}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="base">
|
size="base">
|
||||||
{paid.trial ? t("start_paid_trial") : t("install_paid_app")}
|
{paid.trial ? t("start_paid_trial") : t("subscribe")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
|
import type Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
|
export default function AdminLayout({
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Force redirect on component level
|
||||||
|
useEffect(() => {
|
||||||
|
if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) {
|
||||||
|
router.replace("/settings/my-account/profile");
|
||||||
|
}
|
||||||
|
}, [session, router]);
|
||||||
|
|
||||||
|
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
|
||||||
|
return (
|
||||||
|
<SettingsLayout {...rest}>
|
||||||
|
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
|
||||||
|
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||||
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLayout = (page: React.ReactElement) => <AdminLayout>{page}</AdminLayout>;
|
||||||
@@ -262,15 +262,21 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
const title = booking.title;
|
const title = booking.title;
|
||||||
|
|
||||||
const showRecordingsButtons = !!(booking.isRecorded && isPast && isConfirmed);
|
const showViewRecordingsButton = !!(booking.isRecorded && isPast && isConfirmed);
|
||||||
|
const showCheckRecordingButton =
|
||||||
|
isPast &&
|
||||||
|
isConfirmed &&
|
||||||
|
!booking.isRecorded &&
|
||||||
|
(!booking.location || booking.location === "integrations:daily" || booking?.location?.trim() === "");
|
||||||
|
|
||||||
const showRecordingActions: ActionType[] = [
|
const showRecordingActions: ActionType[] = [
|
||||||
{
|
{
|
||||||
id: "view_recordings",
|
id: "view_recordings",
|
||||||
label: t("view_recordings"),
|
label: showCheckRecordingButton ? t("check_for_recordings") : t("view_recordings"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setViewRecordingsDialogIsOpen(true);
|
setViewRecordingsDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
|
color: showCheckRecordingButton ? "secondary" : "primary",
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -298,7 +304,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
paymentCurrency={booking.payment[0].currency}
|
paymentCurrency={booking.payment[0].currency}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showRecordingsButtons && (
|
{(showViewRecordingsButton || showCheckRecordingButton) && (
|
||||||
<ViewRecordingsDialog
|
<ViewRecordingsDialog
|
||||||
booking={booking}
|
booking={booking}
|
||||||
isOpenDialog={viewRecordingsDialogIsOpen}
|
isOpenDialog={viewRecordingsDialogIsOpen}
|
||||||
@@ -478,7 +484,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
|
{(showViewRecordingsButton || showCheckRecordingButton) && (
|
||||||
|
<TableActions actions={showRecordingActions} />
|
||||||
|
)}
|
||||||
{isCancelled && booking.rescheduled && (
|
{isCancelled && booking.rescheduled && (
|
||||||
<div className="hidden h-full items-center md:flex">
|
<div className="hidden h-full items-center md:flex">
|
||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
@@ -517,8 +525,8 @@ const RecurringBookingsTooltip = ({
|
|||||||
return (
|
return (
|
||||||
recurringDate >= now &&
|
recurringDate >= now &&
|
||||||
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||||
.map((date) => date.toDateString())
|
.map((date) => date.toString())
|
||||||
.includes(recurringDate.toDateString())
|
.includes(recurringDate.toString())
|
||||||
);
|
);
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
@@ -535,8 +543,8 @@ const RecurringBookingsTooltip = ({
|
|||||||
const pastOrCancelled =
|
const pastOrCancelled =
|
||||||
aDate < now ||
|
aDate < now ||
|
||||||
booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||||
.map((date) => date.toDateString())
|
.map((date) => date.toString())
|
||||||
.includes(aDate.toDateString());
|
.includes(aDate.toString());
|
||||||
return (
|
return (
|
||||||
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
||||||
{formatTime(aDate, userTimeFormat, userTimeZone)}
|
{formatTime(aDate, userTimeFormat, userTimeZone)}
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{selectedLocation && LocationOptions}
|
{selectedLocation && LocationOptions}
|
||||||
<DialogFooter className="mt-4">
|
<DialogFooter className="relative">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowLocationModal(false);
|
setShowLocationModal(false);
|
||||||
|
|||||||
@@ -298,6 +298,29 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="onlyShowFirstAvailableSlot"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { value } }) => {
|
||||||
|
const isChecked = value;
|
||||||
|
return (
|
||||||
|
<SettingsToggle
|
||||||
|
toggleSwitchAtTheEnd={true}
|
||||||
|
labelClassName="text-sm"
|
||||||
|
title={t("limit_booking_only_first_slot")}
|
||||||
|
description={t("limit_booking_only_first_slot_description")}
|
||||||
|
checked={isChecked}
|
||||||
|
onCheckedChange={(active) => {
|
||||||
|
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
|
||||||
|
}}
|
||||||
|
switchContainerClassName={classNames(
|
||||||
|
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
|
||||||
|
isChecked && "rounded-b-none"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="durationLimits"
|
name="durationLimits"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
|
|||||||
@@ -357,7 +357,6 @@ export const EventSetupTab = (
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<LocationSelect
|
<LocationSelect
|
||||||
defaultMenuIsOpen={showEmptyLocationSelect}
|
defaultMenuIsOpen={showEmptyLocationSelect}
|
||||||
autoFocus
|
|
||||||
placeholder={t("select")}
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={selectedNewOption}
|
value={selectedNewOption}
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ function EventTypeSingleLayout({
|
|||||||
StartIcon={Code}
|
StartIcon={Code}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
|
namespace={eventType.slug}
|
||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
tooltipSide="bottom"
|
tooltipSide="bottom"
|
||||||
tooltipOffset={4}
|
tooltipOffset={4}
|
||||||
|
|||||||
@@ -156,11 +156,8 @@ const UserProfile = () => {
|
|||||||
{t("few_sentences_about_yourself")}
|
{t("few_sentences_about_yourself")}
|
||||||
</p>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<Button
|
<Button EndIcon={ArrowRight} type="submit" className="mt-8 w-full items-center justify-center">
|
||||||
type="submit"
|
|
||||||
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
|
|
||||||
{t("finish")}
|
{t("finish")}
|
||||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect, Input } from "@calcom/ui";
|
||||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
@@ -76,7 +76,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
<label htmlFor="name" className="text-default mb-2 block text-sm font-medium">
|
<label htmlFor="name" className="text-default mb-2 block text-sm font-medium">
|
||||||
{t("full_name")}
|
{t("full_name")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
{...register("name", {
|
{...register("name", {
|
||||||
required: true,
|
required: true,
|
||||||
})}
|
})}
|
||||||
@@ -85,7 +85,6 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
className="border-default w-full rounded-md border text-sm"
|
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||||
@@ -106,7 +105,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
className="mt-2 w-full rounded-md text-sm"
|
className="mt-2 w-full rounded-md text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-subtle dark:text-inverted mt-3 flex flex-row font-sans text-xs leading-tight">
|
<p className="text-subtle mt-3 flex flex-row font-sans text-xs leading-tight">
|
||||||
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
|
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||||
import type { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
import { AvatarGroup } from "@calcom/ui";
|
import { AvatarGroup } from "@calcom/ui";
|
||||||
@@ -11,6 +12,7 @@ export function UserAvatarGroup(props: UserAvatarProps) {
|
|||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
{...rest}
|
{...rest}
|
||||||
items={users.map((user) => ({
|
items={users.map((user) => ({
|
||||||
|
href: `${CAL_URL}/${user.username}?redirect=false`,
|
||||||
alt: user.name || "",
|
alt: user.name || "",
|
||||||
title: user.name || "",
|
title: user.name || "",
|
||||||
image: getUserAvatarUrl(user),
|
image: getUserAvatarUrl(user),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||||
|
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||||
import type { Team, User } from "@calcom/prisma/client";
|
import type { Team, User } from "@calcom/prisma/client";
|
||||||
import { AvatarGroup } from "@calcom/ui";
|
import { AvatarGroup } from "@calcom/ui";
|
||||||
@@ -10,8 +11,11 @@ type UserAvatarProps = Omit<React.ComponentProps<typeof AvatarGroup>, "items"> &
|
|||||||
|
|
||||||
export function UserAvatarGroupWithOrg(props: UserAvatarProps) {
|
export function UserAvatarGroupWithOrg(props: UserAvatarProps) {
|
||||||
const { users, organization, ...rest } = props;
|
const { users, organization, ...rest } = props;
|
||||||
|
const orgBranding = useOrgBranding();
|
||||||
|
const baseUrl = `${orgBranding?.fullDomain ?? CAL_URL}`;
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
href: baseUrl,
|
||||||
image: `${WEBAPP_URL}/team/${organization.slug}/avatar.png`,
|
image: `${WEBAPP_URL}/team/${organization.slug}/avatar.png`,
|
||||||
alt: organization.name || undefined,
|
alt: organization.name || undefined,
|
||||||
title: organization.name,
|
title: organization.name,
|
||||||
@@ -19,6 +23,7 @@ export function UserAvatarGroupWithOrg(props: UserAvatarProps) {
|
|||||||
].concat(
|
].concat(
|
||||||
users.map((user) => {
|
users.map((user) => {
|
||||||
return {
|
return {
|
||||||
|
href: `${baseUrl}/${user.username}/?redirect=false`,
|
||||||
image: getUserAvatarUrl(user),
|
image: getUserAvatarUrl(user),
|
||||||
alt: user.name || undefined,
|
alt: user.name || undefined,
|
||||||
title: user.name || user.username || "",
|
title: user.name || user.username || "",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{...rest}
|
{...rest}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded"
|
className="text-emphasis focus:ring-emphasis dark:text-muted border-default bg-default h-4 w-4 rounded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ms-2 text-sm">{description}</span>
|
<span className="ms-2 text-sm">{description}</span>
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
import { TrpcProvider } from "_app/_trpc/trpc-provider";
|
||||||
import { dir } from "i18next";
|
import { dir } from "i18next";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { SessionProvider, useSession } from "next-auth/react";
|
import { SessionProvider, useSession } from "next-auth/react";
|
||||||
@@ -97,13 +97,9 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR
|
|||||||
const clientViewerI18n = useViewerI18n(locale);
|
const clientViewerI18n = useViewerI18n(locale);
|
||||||
const i18n = clientViewerI18n.data?.i18n ?? props.i18n;
|
const i18n = clientViewerI18n.data?.i18n ?? props.i18n;
|
||||||
|
|
||||||
if (!i18n || !i18n._nextI18Next) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error AppWithTranslationHoc expects AppProps
|
// @ts-expect-error AppWithTranslationHoc expects AppProps
|
||||||
<AppWithTranslationHoc pageProps={{ _nextI18Next: i18n._nextI18Next }}>
|
<AppWithTranslationHoc pageProps={{ _nextI18Next: i18n?._nextI18Next }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</AppWithTranslationHoc>
|
</AppWithTranslationHoc>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -64,6 +64,23 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
|||||||
requestHeaders.set("x-csp-enforce", "true");
|
requestHeaders.set("x-csp-enforce", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/future/apps/installed")) {
|
||||||
|
const returnTo = req.cookies.get("return-to")?.value;
|
||||||
|
if (returnTo !== undefined) {
|
||||||
|
requestHeaders.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
||||||
|
|
||||||
|
let validPathname = returnTo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
validPathname = new URL(returnTo).pathname;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const nextUrl = url.clone();
|
||||||
|
nextUrl.pathname = validPathname;
|
||||||
|
return NextResponse.redirect(nextUrl, { headers: requestHeaders });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requestHeaders.set("x-pathname", url.pathname);
|
requestHeaders.set("x-pathname", url.pathname);
|
||||||
|
|
||||||
const locale = await getLocale(req);
|
const locale = await getLocale(req);
|
||||||
@@ -101,6 +118,18 @@ export const config = {
|
|||||||
"/apps/routing_forms/:path*",
|
"/apps/routing_forms/:path*",
|
||||||
"/event-types",
|
"/event-types",
|
||||||
"/future/event-types/",
|
"/future/event-types/",
|
||||||
|
"/settings/admin/:path*",
|
||||||
|
"/future/settings/admin/:path*",
|
||||||
|
"/apps/installed/:category/",
|
||||||
|
"/future/apps/installed/:category/",
|
||||||
|
"/apps/:slug/",
|
||||||
|
"/future/apps/:slug/",
|
||||||
|
"/apps/:slug/setup/",
|
||||||
|
"/future/apps/:slug/setup/",
|
||||||
|
"/apps/categories/",
|
||||||
|
"/future/apps/categories/",
|
||||||
|
"/apps/categories/:category/",
|
||||||
|
"/future/apps/categories/:category/",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+24
-5
@@ -71,13 +71,21 @@ if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CRED
|
|||||||
}
|
}
|
||||||
|
|
||||||
const informAboutDuplicateTranslations = () => {
|
const informAboutDuplicateTranslations = () => {
|
||||||
const valueSet = new Set();
|
const valueMap = {};
|
||||||
|
|
||||||
for (const key in englishTranslation) {
|
for (const key in englishTranslation) {
|
||||||
if (valueSet.has(englishTranslation[key])) {
|
const value = englishTranslation[key];
|
||||||
console.warn("\x1b[33mDuplicate value found in:", "\x1b[0m", key);
|
|
||||||
|
if (valueMap[value]) {
|
||||||
|
console.warn(
|
||||||
|
"\x1b[33mDuplicate value found in common.json keys:",
|
||||||
|
"\x1b[0m ",
|
||||||
|
key,
|
||||||
|
"and",
|
||||||
|
valueMap[value]
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
valueSet.add(englishTranslation[key]);
|
valueMap[value] = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -146,11 +154,14 @@ const matcherConfigUserTypeEmbedRoute = {
|
|||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
experimental: {
|
||||||
|
serverComponentsExternalPackages: ["next-i18next"],
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
...i18n,
|
...i18n,
|
||||||
localeDetection: false,
|
localeDetection: false,
|
||||||
},
|
},
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: false,
|
||||||
/* We already do type check on GH actions */
|
/* We already do type check on GH actions */
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: !!process.env.CI,
|
ignoreBuildErrors: !!process.env.CI,
|
||||||
@@ -223,6 +234,9 @@ const nextConfig = {
|
|||||||
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
|
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
|
||||||
// by next.js will be dropped. Doesn't make much sense, but how it is
|
// by next.js will be dropped. Doesn't make much sense, but how it is
|
||||||
fs: false,
|
fs: false,
|
||||||
|
// ignore module resolve errors caused by the server component bundler
|
||||||
|
"pg-native": false,
|
||||||
|
"superagent-proxy": false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -502,6 +516,11 @@ const nextConfig = {
|
|||||||
destination: "/apps/installed/conferencing",
|
destination: "/apps/installed/conferencing",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/apps/installed",
|
||||||
|
destination: "/apps/installed/calendar",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
|
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
|
||||||
...(process.env.NODE_ENV === "development" &&
|
...(process.env.NODE_ENV === "development" &&
|
||||||
// Safer to enable the redirect only when the user is opting to test out organizations
|
// Safer to enable the redirect only when the user is opting to test out organizations
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.5.2",
|
"version": "3.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"ics": "^2.37.0",
|
"ics": "^2.37.0",
|
||||||
"jose": "^4.13.1",
|
"jose": "^4.13.1",
|
||||||
"kbar": "^0.1.0-beta.36",
|
"kbar": "^0.1.0-beta.36",
|
||||||
"libphonenumber-js": "^1.10.12",
|
"libphonenumber-js": "^1.10.51",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lottie-react": "^2.3.1",
|
"lottie-react": "^2.3.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
@@ -130,7 +130,6 @@
|
|||||||
"tailwindcss-radix": "^2.6.0",
|
"tailwindcss-radix": "^2.6.0",
|
||||||
"turndown": "^7.1.1",
|
"turndown": "^7.1.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web3": "^1.7.5",
|
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||||||
// So it doesn't display in the Link (and make tests fail)
|
// So it doesn't display in the Link (and make tests fail)
|
||||||
user: _user,
|
user: _user,
|
||||||
orgSlug: _orgSlug,
|
orgSlug: _orgSlug,
|
||||||
|
redirect: _redirect,
|
||||||
...query
|
...query
|
||||||
} = useRouterQuery();
|
} = useRouterQuery();
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<h1 className="font-cal text-emphasis mb-1 text-3xl" data-testid="name-title">
|
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
|
||||||
{profile.name}
|
{profile.name}
|
||||||
{user.verified && (
|
{user.verified && (
|
||||||
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
|
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
|
||||||
@@ -388,6 +389,16 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
|||||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// if profile only has one public event-type, redirect to it
|
||||||
|
if (eventTypes.length === 1 && context.query.redirect !== "false") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: `/${user.username}/${eventTypes[0].slug}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||||
|
|
||||||
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Booker } from "@calcom/atoms";
|
import { Booker } from "@calcom/atoms";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import {
|
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||||
getBookingForReschedule,
|
|
||||||
getBookingForSeatedEvent,
|
|
||||||
getMultipleDurationValue,
|
|
||||||
} from "@calcom/features/bookings/lib/get-booking";
|
|
||||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
@@ -26,6 +23,16 @@ import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect";
|
|||||||
|
|
||||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||||
|
|
||||||
|
export const getMultipleDurationValue = (
|
||||||
|
multipleDurationConfig: number[] | undefined,
|
||||||
|
queryDuration: string | string[] | null | undefined,
|
||||||
|
defaultValue: number
|
||||||
|
) => {
|
||||||
|
if (!multipleDurationConfig) return null;
|
||||||
|
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
|
||||||
|
return defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Type({
|
export default function Type({
|
||||||
slug,
|
slug,
|
||||||
user,
|
user,
|
||||||
@@ -35,9 +42,10 @@ export default function Type({
|
|||||||
isBrandingHidden,
|
isBrandingHidden,
|
||||||
isSEOIndexable,
|
isSEOIndexable,
|
||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
entity,
|
eventData,
|
||||||
duration,
|
|
||||||
}: PageProps) {
|
}: PageProps) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||||
<BookerSeo
|
<BookerSeo
|
||||||
@@ -46,7 +54,7 @@ export default function Type({
|
|||||||
rescheduleUid={rescheduleUid ?? undefined}
|
rescheduleUid={rescheduleUid ?? undefined}
|
||||||
hideBranding={isBrandingHidden}
|
hideBranding={isBrandingHidden}
|
||||||
isSEOIndexable={isSEOIndexable ?? true}
|
isSEOIndexable={isSEOIndexable ?? true}
|
||||||
entity={entity}
|
entity={eventData.entity}
|
||||||
bookingData={booking}
|
bookingData={booking}
|
||||||
/>
|
/>
|
||||||
<Booker
|
<Booker
|
||||||
@@ -55,8 +63,16 @@ export default function Type({
|
|||||||
bookingData={booking}
|
bookingData={booking}
|
||||||
isAway={away}
|
isAway={away}
|
||||||
hideBranding={isBrandingHidden}
|
hideBranding={isBrandingHidden}
|
||||||
entity={entity}
|
entity={eventData.entity}
|
||||||
duration={duration}
|
durationConfig={eventData.metadata?.multipleDuration}
|
||||||
|
/* TODO: Currently unused, evaluate it is needed-
|
||||||
|
* Possible alternative approach is to have onDurationChange.
|
||||||
|
*/
|
||||||
|
duration={getMultipleDurationValue(
|
||||||
|
eventData.metadata?.multipleDuration,
|
||||||
|
searchParams?.get("duration"),
|
||||||
|
eventData.length
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
@@ -68,7 +84,7 @@ Type.PageWrapper = PageWrapper;
|
|||||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||||
const session = await getServerSession(context);
|
const session = await getServerSession(context);
|
||||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, bookingUid } = context.query;
|
||||||
|
|
||||||
const { ssrInit } = await import("@server/lib/ssr");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
@@ -120,12 +136,14 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
entity: eventData.entity,
|
eventData: {
|
||||||
duration: getMultipleDurationValue(
|
entity: eventData.entity,
|
||||||
eventData.metadata?.multipleDuration,
|
length: eventData.length,
|
||||||
queryDuration,
|
metadata: {
|
||||||
eventData.length
|
...eventData.metadata,
|
||||||
),
|
multipleDuration: [15, 30, 60],
|
||||||
|
},
|
||||||
|
},
|
||||||
booking,
|
booking,
|
||||||
user: usernames.join("+"),
|
user: usernames.join("+"),
|
||||||
slug,
|
slug,
|
||||||
@@ -144,7 +162,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
const session = await getServerSession(context);
|
const session = await getServerSession(context);
|
||||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||||
const username = usernames[0];
|
const username = usernames[0];
|
||||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
const { rescheduleUid, bookingUid } = context.query;
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||||
|
|
||||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||||
@@ -207,15 +225,14 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
booking,
|
booking,
|
||||||
duration: getMultipleDurationValue(
|
eventData: {
|
||||||
eventData.metadata?.multipleDuration,
|
entity: eventData.entity,
|
||||||
queryDuration,
|
length: eventData.length,
|
||||||
eventData.length
|
metadata: eventData.metadata,
|
||||||
),
|
},
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
user: username,
|
user: username,
|
||||||
slug,
|
slug,
|
||||||
entity: eventData.entity,
|
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isBrandingHidden: user?.hideBranding,
|
isBrandingHidden: user?.hideBranding,
|
||||||
isSEOIndexable: user?.allowSEOIndexing,
|
isSEOIndexable: user?.allowSEOIndexing,
|
||||||
|
|||||||
@@ -1,218 +1,63 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiResponse } from "next";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import calcomSignupHandler from "@calcom/feature-auth/signup/handlers/calcomHandler";
|
||||||
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
|
import selfHostedSignupHandler from "@calcom/feature-auth/signup/handlers/selfHostedHandler";
|
||||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
import { type RequestWithUsernameStatus } from "@calcom/features/auth/signup/username";
|
||||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
||||||
import { IS_CALCOM } from "@calcom/lib/constants";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import logger from "@calcom/lib/logger";
|
||||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
|
||||||
import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
|
||||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
function ensureSignupIsEnabled(req: RequestWithUsernameStatus) {
|
||||||
if (req.method !== "POST") {
|
const { token } = signupSchema
|
||||||
return res.status(405).end();
|
.pick({
|
||||||
}
|
token: true,
|
||||||
|
})
|
||||||
|
.parse(req.body);
|
||||||
|
|
||||||
|
// Stil allow signups if there is a team invite
|
||||||
|
if (token) return;
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
|
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
|
||||||
res.status(403).json({ message: "Signup is disabled" });
|
throw new HttpError({
|
||||||
return;
|
statusCode: 403,
|
||||||
}
|
message: "Signup is disabled",
|
||||||
|
|
||||||
const data = req.body;
|
|
||||||
const { email, password, language, token } = signupSchema.parse(data);
|
|
||||||
|
|
||||||
const username = slugify(data.username);
|
|
||||||
const userEmail = email.toLowerCase();
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
res.status(422).json({ message: "Invalid username" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let foundToken: { id: number; teamId: number | null; expires: Date } | null = null;
|
|
||||||
if (token) {
|
|
||||||
foundToken = await prisma.verificationToken.findFirst({
|
|
||||||
where: {
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
expires: true,
|
|
||||||
teamId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!foundToken) {
|
|
||||||
return res.status(401).json({ message: "Invalid Token" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dayjs(foundToken?.expires).isBefore(dayjs())) {
|
|
||||||
return res.status(401).json({ message: "Token expired" });
|
|
||||||
}
|
|
||||||
if (foundToken?.teamId) {
|
|
||||||
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
|
|
||||||
if (!teamUserValidation.isValid) {
|
|
||||||
return res.status(409).json({ message: "Username or email is already taken" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const userValidation = await validateUsername(username, userEmail);
|
|
||||||
if (!userValidation.isValid) {
|
|
||||||
return res.status(409).json({ message: "Username or email is already taken" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPassword = await hashPassword(password);
|
|
||||||
|
|
||||||
if (foundToken && foundToken?.teamId) {
|
|
||||||
const team = await prisma.team.findUnique({
|
|
||||||
where: {
|
|
||||||
id: foundToken.teamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (team) {
|
|
||||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
|
||||||
|
|
||||||
if (IS_CALCOM && (!teamMetadata?.isOrganization || !!team.parentId)) {
|
|
||||||
const checkUsername = await checkPremiumUsername(username);
|
|
||||||
if (checkUsername.premium) {
|
|
||||||
// This signup page is ONLY meant for team invites and local setup. Not for every day users.
|
|
||||||
// In singup redesign/refactor coming up @sean will tackle this to make them the same API/page instead of two.
|
|
||||||
return res.status(422).json({
|
|
||||||
message: "Sign up from https://cal.com/signup to claim your premium username",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identify the org id in an org context signup, either the invited team is an org
|
|
||||||
// or has a parentId, otherwise parentId will be null, making orgId null
|
|
||||||
const orgId = teamMetadata?.isOrganization ? team.id : team.parentId;
|
|
||||||
|
|
||||||
const user = await prisma.user.upsert({
|
|
||||||
where: { email: userEmail },
|
|
||||||
update: {
|
|
||||||
username,
|
|
||||||
password: hashedPassword,
|
|
||||||
emailVerified: new Date(Date.now()),
|
|
||||||
identityProvider: IdentityProvider.CAL,
|
|
||||||
organizationId: orgId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
username,
|
|
||||||
email: userEmail,
|
|
||||||
password: hashedPassword,
|
|
||||||
identityProvider: IdentityProvider.CAL,
|
|
||||||
organizationId: orgId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const membership = await prisma.membership.upsert({
|
|
||||||
where: {
|
|
||||||
userId_teamId: { userId: user.id, teamId: team.id },
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
userId: user.id,
|
|
||||||
teamId: team.id,
|
|
||||||
accepted: true,
|
|
||||||
role: MembershipRole.MEMBER,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
closeComUpsertTeamUser(team, user, membership.role);
|
|
||||||
|
|
||||||
// Accept any child team invites for orgs and create a membership for the org itself
|
|
||||||
if (team.parentId) {
|
|
||||||
// Create (when invite link is used) or Update (when regular email invitation is used) membership for the organization itself
|
|
||||||
await prisma.membership.upsert({
|
|
||||||
where: {
|
|
||||||
userId_teamId: { userId: user.id, teamId: team.parentId },
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
userId: user.id,
|
|
||||||
teamId: team.parentId,
|
|
||||||
accepted: true,
|
|
||||||
role: MembershipRole.MEMBER,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG
|
|
||||||
await prisma.membership.updateMany({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
team: {
|
|
||||||
id: team.parentId,
|
|
||||||
},
|
|
||||||
accepted: false,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Join any other invites
|
|
||||||
await prisma.membership.updateMany({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
team: {
|
|
||||||
parentId: team.parentId,
|
|
||||||
},
|
|
||||||
accepted: false,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup token after use
|
|
||||||
await prisma.verificationToken.delete({
|
|
||||||
where: {
|
|
||||||
id: foundToken.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (IS_CALCOM) {
|
|
||||||
const checkUsername = await checkPremiumUsername(username);
|
|
||||||
if (checkUsername.premium) {
|
|
||||||
res.status(422).json({
|
|
||||||
message: "Sign up from https://cal.com/signup to claim your premium username",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await prisma.user.upsert({
|
|
||||||
where: { email: userEmail },
|
|
||||||
update: {
|
|
||||||
username,
|
|
||||||
password: hashedPassword,
|
|
||||||
emailVerified: new Date(Date.now()),
|
|
||||||
identityProvider: IdentityProvider.CAL,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
username,
|
|
||||||
email: userEmail,
|
|
||||||
password: hashedPassword,
|
|
||||||
identityProvider: IdentityProvider.CAL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await sendEmailVerification({
|
|
||||||
email: userEmail,
|
|
||||||
username,
|
|
||||||
language,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json({ message: "Created user" });
|
function ensureReqIsPost(req: RequestWithUsernameStatus) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 405,
|
||||||
|
message: "Method not allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||||
|
// Use a try catch instead of returning res every time
|
||||||
|
try {
|
||||||
|
ensureReqIsPost(req);
|
||||||
|
ensureSignupIsEnabled(req);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Im not sure its worth merging these two handlers. They are different enough to be separate.
|
||||||
|
* Calcom handles things like creating a stripe customer - which we don't need to do for self hosted.
|
||||||
|
* It also handles things like premium username.
|
||||||
|
* TODO: (SEAN) - Extract a lot of the logic from calcomHandler into a separate file and import it into both handlers.
|
||||||
|
* @zomars: We need to be able to test this with E2E. They way it's done RN it will never run on CI.
|
||||||
|
*/
|
||||||
|
if (IS_PREMIUM_USERNAME_ENABLED) {
|
||||||
|
return await calcomSignupHandler(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await selfHostedSignupHandler(req, res);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof HttpError) {
|
||||||
|
return res.status(e.statusCode).json({ message: e.message });
|
||||||
|
}
|
||||||
|
logger.error(e);
|
||||||
|
return res.status(500).json({ message: "Internal server error" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("MonthlyDigestEmail", {
|
await renderEmail("MonthlyDigestEmail", {
|
||||||
language: t,
|
language: t,
|
||||||
Created: 12,
|
Created: 12,
|
||||||
Completed: 13,
|
Completed: 13,
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" });
|
throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.STRIPE_WEBHOOK_SECRET) {
|
if (!process.env.STRIPE_WEBHOOK_SECRET_APPS) {
|
||||||
throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
|
throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET_APPS" });
|
||||||
}
|
}
|
||||||
const requestBuffer = await buffer(req);
|
const requestBuffer = await buffer(req);
|
||||||
const payload = requestBuffer.toString();
|
const payload = requestBuffer.toString();
|
||||||
|
|
||||||
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
|
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET_APPS);
|
||||||
|
|
||||||
const handler = webhookHandlers[event.type];
|
const handler = webhookHandlers[event.type];
|
||||||
if (handler) {
|
if (handler) {
|
||||||
|
|||||||
@@ -1,24 +1,38 @@
|
|||||||
import type { WebhookTriggerEvents } from "@prisma/client";
|
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||||
|
import { createHmac } from "crypto";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
|
||||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||||
import { defaultHandler } from "@calcom/lib/server";
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z
|
||||||
recordingId: z.string(),
|
.object({
|
||||||
bookingUID: z.string(),
|
version: z.string(),
|
||||||
});
|
type: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
payload: z.object({
|
||||||
|
recording_id: z.string(),
|
||||||
|
end_ts: z.number(),
|
||||||
|
room_name: z.string(),
|
||||||
|
start_ts: z.number(),
|
||||||
|
status: z.string(),
|
||||||
|
|
||||||
|
max_participants: z.number(),
|
||||||
|
duration: z.number(),
|
||||||
|
s3_key: z.string(),
|
||||||
|
}),
|
||||||
|
event_ts: z.number(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const downloadLinkSchema = z.object({
|
const downloadLinkSchema = z.object({
|
||||||
download_link: z.string(),
|
download_link: z.string(),
|
||||||
@@ -39,8 +53,8 @@ const triggerWebhook = async ({
|
|||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
|
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
|
||||||
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
|
||||||
|
|
||||||
|
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||||
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
|
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
|
||||||
|
|
||||||
const subscriberOptions = {
|
const subscriberOptions = {
|
||||||
@@ -62,71 +76,62 @@ const triggerWebhook = async ({
|
|||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkIfUserIsPartOfTheSameTeam = async (
|
const testRequestSchema = z.object({
|
||||||
teamId: number | undefined | null,
|
test: z.enum(["test"]),
|
||||||
userId: number,
|
});
|
||||||
userEmail: string | undefined | null
|
|
||||||
) => {
|
|
||||||
if (!teamId) return false;
|
|
||||||
|
|
||||||
const getUserQuery = () => {
|
|
||||||
if (!!userEmail) {
|
|
||||||
return {
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: userEmail,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
id: userId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const team = await prisma.team.findFirst({
|
|
||||||
where: {
|
|
||||||
id: teamId,
|
|
||||||
members: {
|
|
||||||
some: {
|
|
||||||
user: getUserQuery(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return !!team;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||||
return res.status(405).json({ message: "No SendGrid API key or email" });
|
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||||
}
|
}
|
||||||
const response = schema.safeParse(JSON.parse(req.body));
|
|
||||||
|
|
||||||
if (!response.success) {
|
if (testRequestSchema.safeParse(req.body).success) {
|
||||||
|
return res.status(200).json({ message: "Test request successful" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hmacSecret = process.env.DAILY_WEBHOOK_SECRET;
|
||||||
|
if (!hmacSecret) {
|
||||||
|
return res.status(405).json({ message: "No Daily Webhook Secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = `${req.headers["x-webhook-timestamp"]}.${JSON.stringify(req.body)}`;
|
||||||
|
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
|
||||||
|
const hmac = createHmac("sha256", base64DecodedSecret);
|
||||||
|
const computed_signature = hmac.update(signature).digest("base64");
|
||||||
|
|
||||||
|
if (req.headers["x-webhook-signature"] !== computed_signature) {
|
||||||
|
return res.status(403).json({ message: "Signature does not match" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = schema.safeParse(req.body);
|
||||||
|
|
||||||
|
if (!response.success || response.data.type !== "recording.ready-to-download") {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: "Invalid Payload",
|
message: "Invalid Payload",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { recordingId, bookingUID } = response.data;
|
const { room_name, recording_id, status } = response.data.payload;
|
||||||
const session = await getServerSession({ req, res });
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (status !== "finished") {
|
||||||
return res.status(401).send({
|
return res.status(400).send({
|
||||||
message: "User not logged in",
|
message: "Recording not finished",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const booking = await prisma.booking.findFirst({
|
const bookingReference = await prisma.bookingReference.findFirst({
|
||||||
|
where: { type: "daily_video", uid: room_name, meetingId: room_name },
|
||||||
|
select: { bookingId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bookingReference || !bookingReference.bookingId) {
|
||||||
|
return res.status(404).send({ message: "Booking reference not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const booking = await prisma.booking.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
uid: bookingUID,
|
id: bookingReference.bookingId,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
...bookingMinimalSelect,
|
...bookingMinimalSelect,
|
||||||
@@ -153,9 +158,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!booking || booking.location !== DailyLocationType) {
|
if (!booking || !(booking.location === DailyLocationType || booking?.location?.trim() === "")) {
|
||||||
return res.status(404).send({
|
return res.status(404).send({
|
||||||
message: `Booking of uid ${bookingUID} does not exist or does not contain daily video as location`,
|
message: `Booking of room_name ${room_name} does not exist or does not contain daily video as location`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,26 +180,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
const attendeesList = await Promise.all(attendeesListPromises);
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
|
||||||
const isUserAttendeeOrOrganiser =
|
|
||||||
booking?.user?.id === session.user.id ||
|
|
||||||
attendeesList.find(
|
|
||||||
(attendee) => attendee.id === session.user.id || attendee.email === session.user.email
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isUserAttendeeOrOrganiser) {
|
|
||||||
const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam(
|
|
||||||
booking?.eventType?.teamId,
|
|
||||||
session.user.id,
|
|
||||||
session.user.email
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isUserMemberOfTheTeam) {
|
|
||||||
return res.status(403).send({
|
|
||||||
message: "Unauthorised",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.booking.update({
|
await prisma.booking.update({
|
||||||
where: {
|
where: {
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
@@ -204,7 +189,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
const response = await getDownloadLinkOfCalVideoByRecordingId(recording_id);
|
||||||
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
||||||
const downloadLink = downloadLinkResponse.download_link;
|
const downloadLink = downloadLinkResponse.download_link;
|
||||||
|
|
||||||
@@ -242,17 +227,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
|
|
||||||
|
|
||||||
// send emails to all attendees only when user has team plan
|
// send emails to all attendees only when user has team plan
|
||||||
if (isSendingEmailsAllowed) {
|
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
||||||
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
return res.status(200).json({ message: "Success" });
|
||||||
return res.status(200).json({ message: "Success" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(403).json({ message: "User does not have team plan to send out emails" });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Error in /recorded-daily-video", err);
|
console.error("Error in /recorded-daily-video", err);
|
||||||
return res.status(500).json({ message: "something went wrong" });
|
return res.status(500).json({ message: "something went wrong" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import type Stripe from "stripe";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
session_id: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkoutSessionMetadataSchema = z.object({
|
||||||
|
teamName: z.string(),
|
||||||
|
teamSlug: z.string(),
|
||||||
|
userId: z.string().transform(Number),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateRandomString = () => {
|
||||||
|
return Math.random().toString(36).substring(2, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { session_id } = querySchema.parse(req.query);
|
||||||
|
|
||||||
|
const checkoutSession = await stripe.checkout.sessions.retrieve(session_id, {
|
||||||
|
expand: ["subscription"],
|
||||||
|
});
|
||||||
|
if (!checkoutSession) throw new HttpError({ statusCode: 404, message: "Checkout session not found" });
|
||||||
|
|
||||||
|
const subscription = checkoutSession.subscription as Stripe.Subscription;
|
||||||
|
|
||||||
|
if (checkoutSession.payment_status !== "paid")
|
||||||
|
throw new HttpError({ statusCode: 402, message: "Payment required" });
|
||||||
|
|
||||||
|
// Let's query to ensure that the team metadata carried over from the checkout session.
|
||||||
|
const parseCheckoutSessionMetadata = checkoutSessionMetadataSchema.safeParse(checkoutSession.metadata);
|
||||||
|
|
||||||
|
if (!parseCheckoutSessionMetadata.success) {
|
||||||
|
console.error(
|
||||||
|
"Team metadata not found in checkout session",
|
||||||
|
parseCheckoutSessionMetadata.error,
|
||||||
|
checkoutSession.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkoutSession.metadata?.userId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Can't publish team/org without userId",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkoutSessionMetadata = parseCheckoutSessionMetadata.success
|
||||||
|
? parseCheckoutSessionMetadata.data
|
||||||
|
: {
|
||||||
|
teamName: checkoutSession?.metadata?.teamName ?? generateRandomString(),
|
||||||
|
teamSlug: checkoutSession?.metadata?.teamSlug ?? generateRandomString(),
|
||||||
|
userId: checkoutSession.metadata.userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const team = await prisma.team.create({
|
||||||
|
data: {
|
||||||
|
name: checkoutSessionMetadata.teamName,
|
||||||
|
slug: checkoutSessionMetadata.teamSlug,
|
||||||
|
members: {
|
||||||
|
create: {
|
||||||
|
userId: checkoutSessionMetadata.userId as number,
|
||||||
|
role: MembershipRole.OWNER,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
paymentId: checkoutSession.id,
|
||||||
|
subscriptionId: subscription.id || null,
|
||||||
|
subscriptionItemId: subscription.items.data[0].id || null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync Services: Close.com
|
||||||
|
// closeComUpdateTeam(prevTeam, team);
|
||||||
|
|
||||||
|
// redirect to team screen
|
||||||
|
res.redirect(302, `/settings/teams/${team.id}/onboard-members?event=team_created`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||||
|
});
|
||||||
@@ -105,8 +105,6 @@ async function getIdentityData(req: NextApiRequest) {
|
|||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const identity = await getIdentityData(req);
|
const identity = await getIdentityData(req);
|
||||||
const img = identity?.avatar;
|
const img = identity?.avatar;
|
||||||
// We cache for one day
|
|
||||||
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
|
|
||||||
// If image isn't set or links to this route itself, use default avatar
|
// If image isn't set or links to this route itself, use default avatar
|
||||||
if (!img) {
|
if (!img) {
|
||||||
if (identity?.org) {
|
if (identity?.org) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import type { InferGetServerSidePropsType } from "next";
|
import type { InferGetServerSidePropsType } from "next";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
|
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -13,7 +15,7 @@ import PageWrapper from "@components/PageWrapper";
|
|||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export default function Apps({ categories }: inferSSRProps<typeof getServerSideProps>) {
|
export default function Apps({ categories }: Omit<inferSSRProps<typeof getServerSideProps>, "trpcState">) {
|
||||||
const { t, isLocaleReady } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useReducer } from "react";
|
import { useReducer } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -54,8 +56,8 @@ const IntegrationsContainer = ({
|
|||||||
automation: Share2,
|
automation: Share2,
|
||||||
analytics: BarChart,
|
analytics: BarChart,
|
||||||
payment: CreditCard,
|
payment: CreditCard,
|
||||||
web3: BarChart, // deprecated
|
|
||||||
other: Grid,
|
other: Grid,
|
||||||
|
web3: CreditCard, // deprecated
|
||||||
video: Video, // deprecated
|
video: Video, // deprecated
|
||||||
messaging: Mail,
|
messaging: Mail,
|
||||||
crm: Contact,
|
crm: Contact,
|
||||||
|
|||||||
Loaded 100 of 494 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user