Merge pull request #68 from calcom/feat/sdk-adduser

Feat/sdk adduser
This commit is contained in:
Agusti Fernandez Pardo authored and GitHub committed 2022-05-10 19:53:40 +02:00
commit 3bf5d77782
33 files changed
+740 -271

No files matched your search

+7 -1
View File
@@ -72,4 +72,10 @@ dist
lint-results
# Yarn
yarn-error.log
yarn-error.log
.turbo
.next
.husky
.vscode
.env
+15
View File
@@ -0,0 +1,15 @@
# Signup for self-hosted EE
In order to open the signup, please replace the contents of the file in
`calcom/apps/web/pages/auth/signup.tsx`
with the contents of the new file provided here in the @calcom/api repo at the folder:
`/auth/signup.tsx`
Once you do, if you run your cal.com self-hosted instance and go to:
`http://localhost:3000/auth/signup`
You should see the signup page, and your users should be able to create new accounts.
Please, keep in mind this will create git conflict resolutions if you try to git pull from main, where the signup.tsx file will still be different. We will find a better workaround for this soon in order to make it pain free.
+182
View File
@@ -0,0 +1,182 @@
import { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { EmailField, PasswordField, TextField } from "@calcom/ui/form/fields";
import { HeadSeo } from "@calcom/web/components/seo/head-seo";
import { asStringOrNull } from "@calcom/web/lib/asStringOrNull";
import { WEBSITE_URL, WEBAPP_URL } from "@calcom/web/lib/config/constants";
import prisma from "@calcom/web/lib/prisma";
import { isSAMLLoginEnabled } from "@calcom/web/lib/saml";
import { IS_GOOGLE_LOGIN_ENABLED } from "@calcom/web/server/lib/constants";
import { ssrInit } from "@calcom/web/server/lib/ssr";
type FormValues = {
username: string;
email: string;
password: string;
passwordcheck: string;
apiError: string;
};
export default function Signup() {
const { t } = useLocale();
const router = useRouter();
const methods = useForm<FormValues>();
const {
register,
formState: { errors, isSubmitting },
} = methods;
const handleErrors = async (resp: Response) => {
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.message);
}
};
const signUp: SubmitHandler<FormValues> = async (data) => {
await fetch("/api/auth/signup", {
body: JSON.stringify({
...data,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
})
.then(handleErrors)
.then(
async () =>
await signIn("Cal.com", {
callbackUrl: (`${WEBSITE_URL}/${router.query.callbackUrl}` || "") as string,
})
)
.catch((err) => {
methods.setError("apiError", { message: err.message });
});
};
return (
<div
className="flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="font-cal text-center text-3xl font-extrabold text-gray-900">
{t("create_your_account")}
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="mx-2 bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(signUp)} className="space-y-6 bg-white">
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
<div className="space-y-2">
<TextField
addOnLeading={
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span>
}
labelProps={{ className: "block text-sm font-medium text-gray-700" }}
className="block w-full min-w-0 flex-grow rounded-none rounded-r-sm border-gray-300 lowercase focus:border-black focus:ring-black sm:text-sm"
{...register("username")}
required
/>
<EmailField
{...register("email")}
className="mt-1 block w-full rounded-md border border-gray-300 bg-gray-100 px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
}}
{...register("password")}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
<PasswordField
label={t("confirm_password")}
labelProps={{
className: "block text-sm font-medium text-gray-700",
}}
{...register("passwordcheck", {
validate: (value) =>
value === methods.watch("password") || (t("error_password_mismatch") as string),
})}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
</div>
<div className="flex space-x-2 rtl:space-x-reverse">
<Button loading={isSubmitting} className="w-7/12 justify-center">
{t("create_account")}
</Button>
<Button
color="secondary"
className="w-5/12 justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: (`${WEBAPP_URL}/${router.query.callbackUrl}` || "") as string,
})
}>
{t("login_instead")}
</Button>
</div>
</form>
</FormProvider>
</div>
</div>
</div>
);
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const ssr = await ssrInit(ctx);
const token = asStringOrNull(ctx.query.token);
if (token) {
const verificationToken = await prisma.verificationToken.findUnique({
where: {
token,
},
});
if (verificationToken) {
const existingUser = await prisma.user.findFirst({
where: {
AND: [
{
email: verificationToken?.identifier,
},
{
emailVerified: {
not: null,
},
},
],
},
});
if (existingUser) {
return {
redirect: {
permanent: false,
destination: "/auth/login?callbackUrl=" + `${WEBAPP_URL}/${ctx.query.callbackUrl}`,
},
};
}
}
}
return {
props: {
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
trpcState: ssr.dehydrate(),
},
};
};
+2 -2
View File
@@ -28,8 +28,8 @@ const schemaBookingReferenceCreateParams = z
type: z.string(),
uid: z.string(),
meetingId: z.string(),
meetingPassword: z.string(),
meetingUrl: z.string(),
meetingPassword: z.string().optional(),
meetingUrl: z.string().optional(),
deleted: z.boolean(),
})
.strict();
+2 -2
View File
@@ -1,5 +1,5 @@
import * as tzdb from "tzdata";
import tzdata from "tzdata";
import * as z from "zod";
// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdb.zones).includes(tz));
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz));
+2 -1
View File
@@ -77,6 +77,7 @@ export const schemaUserBaseBodyParams = User.pick({
const schemaUserEditParams = z.object({
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
timeZone: timeZone.optional(),
bufferTime: z.number().min(0).max(86400).optional(),
startTime: z.number().min(0).max(86400).optional(),
@@ -87,7 +88,7 @@ const schemaUserEditParams = z.object({
.number()
.refine((id: number) => id > 0)
.optional(),
locale: z.nativeEnum(locales),
locale: z.nativeEnum(locales).optional(),
});
// @note: These are the values that are editable via PATCH method on the user Model,
+10 -10
View File
@@ -1,4 +1,4 @@
{
{
"name": "@calcom/api",
"version": "1.0.0",
"description": "Public API for Cal.com",
@@ -13,27 +13,27 @@
"build": "next build",
"lint": "next lint",
"lint-fix": "next lint --fix && prettier --write .",
"test": "jest --detectOpenHandles",
"test": "jest --detectOpenHandles --passWithNoTests",
"type-check": "tsc --pretty --noEmit",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"devDependencies": {
"@calcom/tsconfig": "*",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"babel-jest": "^27.5.1",
"jest": "^27.5.1",
"@typescript-eslint/eslint-plugin": "^5.22.0",
"babel-jest": "^28.0.3",
"jest": "^28.0.3",
"node-mocks-http": "^1.11.0"
},
"dependencies": {
"@calcom/prisma": "*",
"@sentry/nextjs": "^6.19.3",
"@sentry/nextjs": "^6.19.7",
"modify-response-middleware": "^1.1.0",
"next": "^12.1.4",
"next": "^12.1.6",
"next-api-middleware": "^1.0.1",
"next-swagger-doc": "^0.2.1",
"next-swagger-doc": "^0.3.4",
"next-transpile-modules": "^9.0.0",
"next-validations": "^0.1.11",
"typescript": "^4.6.3",
"next-validations": "^0.2.0",
"typescript": "^4.6.4",
"tzdata": "^1.0.30"
}
}
+8 -10
View File
@@ -17,7 +17,7 @@ export async function attendeeById(
const safeQuery = schemaQueryIdParseInt.safeParse(query);
if (!safeQuery.success) {
res.status(400).json({ error: safeQuery.error });
throw new Error("Invalid request query", safeQuery.error);
return;
}
const userBookingsAttendeeIds = await prisma.booking
// Find all user bookings, including attendees
@@ -41,6 +41,7 @@ export async function attendeeById(
* @swagger
* /attendees/{id}:
* get:
* operationId: getAttendeeById
* summary: Find an attendee
* parameters:
* - in: path
@@ -48,7 +49,7 @@ export async function attendeeById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the attendee to get
* description: ID of the attendee to get
* example: 3
* tags:
* - attendees
@@ -77,6 +78,7 @@ export async function attendeeById(
* /attendees/{id}:
* patch:
* summary: Edit an existing attendee
* operationId: editAttendeeById
* requestBody:
* description: Edit an existing attendee related to one of your bookings
* required: true
@@ -84,11 +86,6 @@ export async function attendeeById(
* application/json:
* schema:
* type: object
* required:
* - bookingId
* - name
* - email
* - timeZone
* properties:
* email:
* type: string
@@ -106,7 +103,7 @@ export async function attendeeById(
* type: integer
* example: 3
* required: true
* description: Numeric ID of the attendee to edit
* description: ID of the attendee to edit
* tags:
* - attendees
* responses:
@@ -121,7 +118,7 @@ export async function attendeeById(
const safeBody = schemaAttendeeEditBodyParams.safeParse(body);
if (!safeBody.success) {
res.status(400).json({ message: "Bad request", error: safeBody.error });
throw new Error("Invalid request body");
return;
}
await prisma.attendee
.update({ where: { id: safeQuery.data.id }, data: safeBody.data })
@@ -138,6 +135,7 @@ export async function attendeeById(
* @swagger
* /attendees/{id}:
* delete:
* operationId: removeAttendeeById
* summary: Remove an existing attendee
* parameters:
* - in: path
@@ -145,7 +143,7 @@ export async function attendeeById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the attendee to delete
* description: ID of the attendee to delete
* tags:
* - attendees
* responses:
+10 -10
View File
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import db from "@calcom/prisma";
import { withMiddleware } from "@lib/helpers/withMiddleware";
import { AttendeeResponse, AttendeesResponse } from "@lib/types";
@@ -10,7 +10,7 @@ async function createOrlistAllAttendees(
{ method, userId, body }: NextApiRequest,
res: NextApiResponse<AttendeesResponse | AttendeeResponse>
) {
const userBookings = await prisma.booking.findMany({
const userBookings = await db.booking.findMany({
where: {
userId,
},
@@ -24,6 +24,7 @@ async function createOrlistAllAttendees(
* @swagger
* /attendees:
* get:
* operationId: listAttendees
* summary: Find all attendees
* tags:
* - attendees
@@ -47,6 +48,7 @@ async function createOrlistAllAttendees(
* @swagger
* /attendees:
* post:
* operationId: addAttendee
* summary: Creates a new attendee
* requestBody:
* description: Create a new attendee related to one of your bookings
@@ -85,21 +87,19 @@ async function createOrlistAllAttendees(
*/
const safePost = schemaAttendeeCreateBodyParams.safeParse(body);
if (!safePost.success) {
res.status(400).json({ error: safePost.error });
throw new Error("Invalid request body", safePost.error);
res.status(400).json({ message: "Invalid request body", error: safePost.error });
return;
}
const userWithBookings = await prisma.user.findUnique({
where: { id: userId },
include: { bookings: true },
});
const userWithBookings = await db.user.findUnique({ where: { id: userId }, include: { bookings: true } });
if (!userWithBookings) {
throw new Error("User not found");
res.status(404).json({ message: "User not found" });
return;
}
const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat();
// Here we make sure to only return attendee's of the user's own bookings.
if (!userBookingIds.includes(safePost.data.bookingId)) res.status(401).json({ message: "Unauthorized" });
else {
const data = await prisma.attendee.create({
const data = await db.attendee.create({
data: {
email: safePost.data.email,
name: safePost.data.name,
+34 -8
View File
@@ -18,7 +18,10 @@ export async function availabilityById(
res: NextApiResponse<AvailabilityResponse>
) {
const safeQuery = schemaQueryIdParseInt.safeParse(query);
if (!safeQuery.success) throw new Error("Invalid request query", safeQuery.error);
if (!safeQuery.success) {
res.status(400).json({ message: "Your query is invalid", error: safeQuery.error });
return;
}
const data = await prisma.availability.findMany({ where: { userId } });
const availabiltiesIds = data.map((availability) => availability.id);
if (!availabiltiesIds.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" });
@@ -28,6 +31,7 @@ export async function availabilityById(
* @swagger
* /availabilities/{id}:
* get:
* operationId: getAvailabilityById
* summary: Find an availability
* parameters:
* - in: path
@@ -35,7 +39,7 @@ export async function availabilityById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the availability to get
* description: ID of the availability to get
* tags:
* - availabilities
* externalDocs:
@@ -44,9 +48,7 @@ export async function availabilityById(
* 200:
* description: OK
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: Availability was not found
* description: Unathorized
*/
case "GET":
await prisma.availability
@@ -61,14 +63,32 @@ export async function availabilityById(
* @swagger
* /availabilities/{id}:
* patch:
* operationId: editAvailabilityById
* summary: Edit an existing availability
* requestBody:
* description: Edit an existing availability related to one of your bookings
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* days:
* type: array
* example: email@example.com
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the availability to edit
* description: ID of the availability to edit
* tags:
* - availabilities
* externalDocs:
@@ -82,8 +102,13 @@ export async function availabilityById(
* description: Authorization information is missing or invalid.
*/
case "PATCH":
console.log(body);
const safeBody = schemaAvailabilityEditBodyParams.safeParse(body);
if (!safeBody.success) throw new Error("Invalid request body");
if (!safeBody.success) {
console.log(safeBody.error);
res.status(400).json({ message: "Bad request" + safeBody.error, error: safeBody.error });
return;
}
const userEventTypes = await prisma.eventType.findMany({ where: { userId } });
const userEventTypesIds = userEventTypes.map((event) => event.id);
if (safeBody.data.eventTypeId && !userEventTypesIds.includes(safeBody.data.eventTypeId)) {
@@ -109,6 +134,7 @@ export async function availabilityById(
* @swagger
* /availabilities/{id}:
* delete:
* operationId: removeAvailabilityById
* summary: Remove an existing availability
* parameters:
* - in: path
@@ -116,7 +142,7 @@ export async function availabilityById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the availability to delete
* description: ID of the availability to delete
* tags:
* - availabilities
* externalDocs:
+27 -1
View File
@@ -18,6 +18,7 @@ async function createOrlistAllAvailabilities(
* @swagger
* /availabilities:
* get:
* operationId: listAvailabilities
* summary: Find all availabilities
* tags:
* - availabilities
@@ -45,7 +46,28 @@ async function createOrlistAllAvailabilities(
* @swagger
* /availabilities:
* post:
* operationId: addAvailability
* summary: Creates a new availability
* requestBody:
* description: Edit an existing availability related to one of your bookings
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - startTime
* - endTime
* properties:
* days:
* type: array
* example: email@example.com
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* tags:
* - availabilities
* externalDocs:
@@ -59,7 +81,11 @@ async function createOrlistAllAvailabilities(
* description: Authorization information is missing or invalid.
*/
const safe = schemaAvailabilityCreateBodyParams.safeParse(body);
if (!safe.success) throw new Error("Invalid request body");
if (!safe.success) {
res.status(400).json({ message: "Your request is invalid", error: safe.error });
return;
}
// FIXME: check for eventTypeId ad scheduleId ownership if passed
const data = await prisma.availability.create({ data: { ...safe.data, userId } });
const availability = schemaAvailabilityReadPublic.parse(data);
+146 -105
View File
@@ -1,4 +1,3 @@
import { BookingModel } from "@/../../packages/prisma/zod";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
@@ -20,95 +19,138 @@ export async function bookingReferenceById(
) {
const safeQuery = schemaQueryIdParseInt.safeParse(query);
if (!safeQuery.success) throw new Error("Invalid request query", safeQuery.error);
const userWithBookings = await prisma.user.findUnique({
where: { id: userId },
include: { bookings: true },
});
if (!userWithBookings) throw new Error("User not found");
const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat();
const bookingReference = await prisma.bookingReference.findUnique({ where: { id: safeQuery.data.id } });
if (!bookingReference?.bookingId) throw new Error("BookingReference: bookingId not found");
if (userBookingIds.includes(bookingReference.bookingId)) {
switch (method) {
case "GET":
/**
* @swagger
* /booking-references/{id}:
* get:
* summary: Find a booking reference
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the booking reference to get
* tags:
* - booking-references
* responses:
* 200:
* description: OK
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: BookingReference was not found
*/
// const userWithBookings = await prisma.user.findUnique({
// where: { id: userId },
// include: { bookings: true },
// });
// if (!userWithBookings) throw new Error("User not found");
// const userBookingIds = userWithBookings.bookings.map((booking: { id: number }) => booking.id).flat();
// console.log(userBookingIds);
// const bookingReferences = await prisma.bookingReference
// .findMany({ where: { id: { in: userBookingIds } } })
// .then((bookingReferences) => {
// console.log(bookingReferences);
// return bookingReferences.map((bookingReference) => bookingReference.id);
// });
await prisma.bookingReference
.findUnique({ where: { id: safeQuery.data.id } })
.then((data) => schemaBookingReferenceReadPublic.parse(data))
.then((booking_reference) => res.status(200).json({ booking_reference }))
.catch((error: Error) =>
res.status(404).json({
message: `BookingReference with id: ${safeQuery.data.id} not found`,
error,
})
);
// res.status(400).json({ message: "Booking reference not found" });
// return;
// }
// if (userBookingIds.includes(safeQuery.data.id)) {
// if (!bookingReferences?.includes(safeQuery.data.id)) {
// throw new Error("BookingReference: bookingId not found");
switch (method) {
case "GET":
/**
* @swagger
* /booking-references/{id}:
* get:
* operationId: getBookingReferenceById
* summary: Find a booking reference
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the booking reference to get
* tags:
* - booking-references
* responses:
* 200:
* description: OK
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: BookingReference was not found
*/
// console.log(safeQuery.data.id);
const bookingReference = await prisma.bookingReference.findFirst().catch((error: Error) => {
console.log("hoerr:", error);
});
// console.log(bookingReference);
if (!bookingReference) res.status(404).json({ message: "Booking reference not found" });
else res.status(200).json({ booking_reference: bookingReference });
break;
case "PATCH":
/**
* @swagger
* /booking-references/{id}:
* patch:
* summary: Edit an existing booking reference
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the booking reference to edit
* tags:
* - booking-references
* responses:
* 201:
* description: OK, bookingReference edited successfuly
* 400:
* description: Bad request. BookingReference body is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
// .then((data) => {
// console.log(data);
// return schemaBookingReferenceReadPublic.parse(data);
// })
// .then((booking_reference) => res.status(200).json({ booking_reference }))
// .catch((error: Error) => {
// console.log(error);
// res.status(404).json({
// message: `BookingReference with id: ${safeQuery.data.id} not found`,
// error,
// });
// });
break;
case "PATCH":
/**
* @swagger
* /booking-references/{id}:
* patch:
* operationId: editBookingReferenceById
* summary: Edit an existing booking reference
* requestBody:
* description: Edit an existing booking reference related to one of your bookings
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* days:
* type: array
* example: email@example.com
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID of the booking reference to edit
* tags:
* - booking-references
* responses:
* 201:
* description: OK, safeBody.data edited successfuly
* 400:
* description: Bad request. BookingReference body is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
const safeBody = schemaBookingEditBodyParams.safeParse(body);
if (!safeBody.success) {
throw new Error("Invalid request body");
}
await prisma.bookingReference
.update({ where: { id: safeQuery.data.id }, data: safeBody.data })
.then((data) => schemaBookingReferenceReadPublic.parse(data))
.then((booking_reference) => res.status(200).json({ booking_reference }))
.catch((error: Error) =>
res.status(404).json({
message: `BookingReference with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
const safeBody = schemaBookingEditBodyParams.safeParse(body);
if (!safeBody.success) {
console.log(safeBody.error);
res.status(400).json({ message: "Invalid request body", error: safeBody.error });
return;
// throw new Error("Invalid request body");
}
await prisma.bookingReference
.update({ where: { id: safeQuery.data.id }, data: safeBody.data })
.then((data) => schemaBookingReferenceReadPublic.parse(data))
.then((booking_reference) => res.status(200).json({ booking_reference }))
.catch((error: Error) =>
res.status(404).json({
message: `BookingReference with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
case "DELETE":
/**
* @swagger
* /booking-references/{id}:
* delete:
* operationId: removeBookingReferenceById
* summary: Remove an existing booking reference
* parameters:
* - in: path
@@ -116,7 +158,7 @@ export async function bookingReferenceById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the booking reference to delete
* description: ID of the booking reference to delete
* tags:
* - booking-references
* responses:
@@ -127,29 +169,28 @@ export async function bookingReferenceById(
* 401:
* description: Authorization information is missing or invalid.
*/
case "DELETE":
await prisma.bookingReference
.delete({
where: { id: safeQuery.data.id },
await prisma.bookingReference
.delete({
where: { id: safeQuery.data.id },
})
.then(() =>
res.status(200).json({
message: `BookingReference with id: ${safeQuery.data.id} deleted`,
})
.then(() =>
res.status(200).json({
message: `BookingReference with id: ${safeQuery.data.id} deleted`,
})
)
.catch((error: Error) =>
res.status(404).json({
message: `BookingReference with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
)
.catch((error: Error) =>
res.status(404).json({
message: `BookingReference with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
default:
res.status(405).json({ message: "Method not allowed" });
break;
}
} else res.status(401).json({ message: "Unauthorized" });
default:
res.status(405).json({ message: "Method not allowed" });
break;
}
// } else res.status(401).json({ message: "Unauthorized" });
}
export default withMiddleware("HTTP_GET_DELETE_PATCH")(
+34 -1
View File
@@ -24,6 +24,7 @@ async function createOrlistAllBookingReferences(
* @swagger
* /booking-references:
* get:
* operationId: listBookingReferences
* summary: Find all booking references
* tags:
* - booking-references
@@ -51,7 +52,37 @@ async function createOrlistAllBookingReferences(
* @swagger
* /booking-references:
* post:
* operationId: addBookingReference
* summary: Creates a new booking reference
* requestBody:
* description: Create a new booking reference related to one of your bookings
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - type
* - uid
* - meetindId
* - bookingId
* - deleted
* properties:
* deleted:
* type: boolean
* example: false
* uid:
* type: string
* example: '123456789'
* type:
* type: string
* example: email@example.com
* bookingId:
* type: number
* example: 1
* meetingId:
* type: string
* example: 'meeting-id'
* tags:
* - booking-references
* responses:
@@ -64,7 +95,9 @@ async function createOrlistAllBookingReferences(
*/
const safe = schemaBookingCreateBodyParams.safeParse(body);
if (!safe.success) {
throw new Error("Invalid request body");
res.status(400).json({ message: "Bad request. BookingReference body is invalid", error: safe.error });
return;
// throw new Error("Invalid request body");
}
const userWithBookings = await prisma.user.findUnique({
+28 -5
View File
@@ -30,13 +30,14 @@ export async function bookingById(
* /bookings/{id}:
* get:
* summary: Find a booking
* operationId: getBookingById
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the booking to get
* description: ID of the booking to get
* tags:
* - bookings
* responses:
@@ -64,13 +65,31 @@ export async function bookingById(
* /bookings/{id}:
* patch:
* summary: Edit an existing booking
* operationId: editBookingById
* requestBody:
* description: Edit an existing booking related to one of your event-types
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* example: 15min
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the booking to edit
* description: ID of the booking to edit
* tags:
* - bookings
* responses:
@@ -84,14 +103,17 @@ export async function bookingById(
case "PATCH":
const safeBody = schemaBookingEditBodyParams.safeParse(body);
if (!safeBody.success) {
throw new Error("Invalid request body");
console.log(safeBody.error);
res.status(400).json({ message: "Bad request", error: safeBody.error });
return;
// throw new Error("Invalid request body");
}
await prisma.booking
.update({
where: { id: safeQuery.data.id },
data: safeBody.data,
})
.then((data) => schemaBookingReadPublic.parse(data))
// .then((data) => schemaBookingReadPublic.parse(data))
.then((booking) => res.status(200).json({ booking }))
.catch((error: Error) =>
res.status(404).json({
@@ -105,13 +127,14 @@ export async function bookingById(
* /bookings/{id}:
* delete:
* summary: Remove an existing booking
* operationId: removeBookingById
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the booking to delete
* description: ID of the booking to delete
* tags:
* - bookings
* responses:
+28 -2
View File
@@ -16,6 +16,7 @@ async function createOrlistAllBookings(
* /bookings:
* get:
* summary: Find all bookings
* operationId: listBookings
* tags:
* - bookings
* responses:
@@ -41,6 +42,24 @@ async function createOrlistAllBookings(
* /bookings:
* post:
* summary: Creates a new booking
* operationId: addBooking
* requestBody:
* description: Edit an existing booking related to one of your event-types
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* example: 15min
* startTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* endTime:
* type: string
* example: 1970-01-01T17:00:00.000Z
* tags:
* - bookings
* responses:
@@ -52,18 +71,25 @@ async function createOrlistAllBookings(
* description: Authorization information is missing or invalid.
*/
const safe = schemaBookingCreateBodyParams.safeParse(body);
if (!safe.success) throw new Error("Invalid request body");
if (!safe.success) {
console.log(safe.error);
res.status(400).json({ message: "Bad request. Booking body is invalid." });
return;
// throw new Error("Invalid request body");
}
const data = await prisma.booking.create({ data: { ...safe.data, userId } });
const booking = schemaBookingReadPublic.parse(data);
if (booking) res.status(201).json({ booking, message: "Booking created successfully" });
else
(error: Error) =>
(error: Error) => {
console.log(error);
res.status(400).json({
message: "Could not create new booking",
error,
});
};
} else res.status(405).json({ message: `Method ${method} not allowed` });
}
+3 -3
View File
@@ -24,7 +24,7 @@ import {
* schema:
* type: integer
* required: true
* description: Numeric ID of the eventTypeCustomInput to get
* description: ID of the eventTypeCustomInput to get
* tags:
* - custom-inputs
* responses:
@@ -42,7 +42,7 @@ import {
* schema:
* type: integer
* required: true
* description: Numeric ID of the eventTypeCustomInput to edit
* description: ID of the eventTypeCustomInput to edit
* tags:
* - custom-inputs
* responses:
@@ -60,7 +60,7 @@ import {
* schema:
* type: integer
* required: true
* description: Numeric ID of the eventTypeCustomInput to delete
* description: ID of the eventTypeCustomInput to delete
* tags:
* - custom-inputs
* responses:
+5 -5
View File
@@ -38,7 +38,7 @@ export async function destionationCalendarById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the destination calendar to get
* description: ID of the destination calendar to get
* tags:
* - destination-calendars
* responses:
@@ -56,7 +56,7 @@ export async function destionationCalendarById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the destination calendar to edit
* description: ID of the destination calendar to edit
* tags:
* - destination-calendars
* responses:
@@ -74,7 +74,7 @@ export async function destionationCalendarById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the destination calendar to delete
* description: ID of the destination calendar to delete
* tags:
* - destination-calendars
* responses:
@@ -108,7 +108,7 @@ export async function destionationCalendarById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the destination calendar to edit
* description: ID of the destination calendar to edit
* tags:
* - destination-calendars
* responses:
@@ -145,7 +145,7 @@ export async function destionationCalendarById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the destination calendar to delete
* description: ID of the destination calendar to delete
* tags:
* - destination-calendars
* responses:
+20 -3
View File
@@ -6,11 +6,11 @@ import { NextApiRequest, NextApiResponse } from "next/types";
const swaggerHandler = withSwagger({
definition: {
openapi: "3.0.0",
openapi: "3.0.3",
servers: [
{ url: "https://api.cal.com/v1" },
{ url: "https://api.cal.dev/v1" },
{ url: "http://localhost:3002/v1" },
{ url: "https://api.cal.dev/v1" },
{ url: "https://api.cal.com/v1" },
],
externalDocs: {
url: "https://docs.cal.com",
@@ -24,6 +24,22 @@ const swaggerHandler = withSwagger({
securitySchemes: { ApiKeyAuth: { type: "apiKey", in: "query", name: "apiKey" } },
},
security: [{ ApiKeyAuth: [] }],
tags: [
{ name: "users" },
{ name: "event-types" },
{ name: "bookings" },
{ name: "attendees" },
{ name: "payments" },
{ name: "schedules" },
{ name: "teams" },
{ name: "memberships" },
{ name: "availabilities" },
{ name: "custom-inputs" },
{ name: "event-references" },
{ name: "booking-references" },
{ name: "destination-calendars" },
{ name: "selected-calendars" },
],
},
apiFolder: "pages/api",
});
@@ -41,6 +57,7 @@ export default use(
);
if (content) {
const parsed = JSON.parse(content);
// HACK: This is a hack to fix the swagger-ui issue with the extra channels property.
delete parsed.channels;
return Buffer.from(JSON.stringify(parsed));
}
+3 -3
View File
@@ -43,7 +43,7 @@ export async function dailyEventReferenceById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the event reference to get
* description: ID of the event reference to get
* tags:
* - event-references
* responses:
@@ -78,7 +78,7 @@ export async function dailyEventReferenceById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the event reference to edit
* description: ID of the event reference to edit
* tags:
* - event-references
* responses:
@@ -116,7 +116,7 @@ export async function dailyEventReferenceById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the event reference to delete
* description: ID of the event reference to delete
* tags:
* - event-references
* responses:
+7 -3
View File
@@ -25,14 +25,16 @@ export async function eventTypeById(
* @swagger
* /event-types/{id}:
* get:
* operationId: getEventTypeById
* summary: Find a eventType
* parameters:
* - in: path
* name: id
* example: 4
* schema:
* type: integer
* required: true
* description: Numeric ID of the eventType to get
* description: ID of the eventType to get
* security:
* - ApiKeyAuth: []
* tags:
@@ -63,6 +65,7 @@ export async function eventTypeById(
* @swagger
* /event-types/{id}:
* patch:
* operationId: editEventTypeById
* summary: Edit an existing eventType
* parameters:
* - in: path
@@ -70,7 +73,7 @@ export async function eventTypeById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the eventType to edit
* description: ID of the eventType to edit
* security:
* - ApiKeyAuth: []
* tags:
@@ -105,6 +108,7 @@ export async function eventTypeById(
* @swagger
* /event-types/{id}:
* delete:
* operationId: removeEventTypeById
* summary: Remove an existing eventType
* parameters:
* - in: path
@@ -112,7 +116,7 @@ export async function eventTypeById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the eventType to delete
* description: ID of the eventType to delete
* security:
* - ApiKeyAuth: []
* tags:
+1 -1
View File
@@ -21,7 +21,7 @@ import {
* schema:
* type: integer
* required: true
* description: Numeric ID of the payment to get
* description: ID of the payment to get
* tags:
* - payments
* responses:
+6 -3
View File
@@ -26,6 +26,7 @@ export async function scheduleById(
* @swagger
* /schedules/{id}:
* get:
* operationId: getScheduleById
* summary: Find a schedule
* parameters:
* - in: path
@@ -33,7 +34,7 @@ export async function scheduleById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the schedule to get
* description: ID of the schedule to get
* tags:
* - schedules
* responses:
@@ -61,6 +62,7 @@ export async function scheduleById(
* @swagger
* /schedules/{id}:
* patch:
* operationId: editScheduleById
* summary: Edit an existing schedule
* parameters:
* - in: path
@@ -68,7 +70,7 @@ export async function scheduleById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the schedule to edit
* description: ID of the schedule to edit
* tags:
* - schedules
* responses:
@@ -99,6 +101,7 @@ export async function scheduleById(
* @swagger
* /schedules/{id}:
* delete:
* operationId: removeScheduleById
* summary: Remove an existing schedule
* parameters:
* - in: path
@@ -106,7 +109,7 @@ export async function scheduleById(
* schema:
* type: integer
* required: true
* description: Numeric ID of the schedule to delete
* description: ID of the schedule to delete
* tags:
* - schedules
* responses:
+2
View File
@@ -15,6 +15,7 @@ async function createOrlistAllSchedules(
* @swagger
* /schedules:
* get:
* operationId: listSchedules
* summary: Find all schedules
* tags:
* - schedules
@@ -40,6 +41,7 @@ async function createOrlistAllSchedules(
* @swagger
* /schedules:
* post:
* operationId: addSchedule
* summary: Creates a new schedule
* tags:
* - schedules
+4 -1
View File
@@ -26,7 +26,8 @@ export async function selectedCalendarById(
* @swagger
* /selected-calendars/{userId}_{integration}_{externalId}:
* get:
* summary: Find a selected calendar
* operationId: getSelectedCalendarById
* summary: Find a selected calendar by providing the compoundId(userId_integration_externalId) separated by `_`
* parameters:
* - in: path
* name: userId
@@ -81,6 +82,7 @@ export async function selectedCalendarById(
* @swagger
* /selected-calendars/{userId}_{integration}_{externalId}:
* patch:
* operationId: editSelectedCalendarById
* summary: Edit a selected calendar
* parameters:
* - in: path
@@ -139,6 +141,7 @@ export async function selectedCalendarById(
* @swagger
* /selected-calendars/{userId}_{integration}_{externalId}:
* delete:
* operationId: removeSelectedCalendarById
* summary: Remove a selected calendar
* parameters:
* - in: path
+2
View File
@@ -18,6 +18,7 @@ async function createOrlistAllSelectedCalendars(
* @swagger
* /selected-calendars:
* get:
* operationId: listSelectedCalendars
* summary: Find all selected calendars
* tags:
* - selected-calendars
@@ -45,6 +46,7 @@ async function createOrlistAllSelectedCalendars(
* @swagger
* /selected-calendars:
* get:
* operationId: addSelectedCalendars
* summary: Find all selected calendars
* tags:
* - selected-calendars
+6 -3
View File
@@ -14,6 +14,7 @@ import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team";
* @swagger
* /teams/{id}:
* get:
* operationId: getTeamById
* summary: Find a team
* parameters:
* - in: path
@@ -21,7 +22,7 @@ import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team";
* schema:
* type: integer
* required: true
* description: Numeric ID of the team to get
* description: ID of the team to get
* tags:
* - teams
* responses:
@@ -32,6 +33,7 @@ import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team";
* 404:
* description: Team was not found
* patch:
* operationId: editTeamById
* summary: Edit an existing team
* parameters:
* - in: path
@@ -39,7 +41,7 @@ import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team";
* schema:
* type: integer
* required: true
* description: Numeric ID of the team to edit
* description: ID of the team to edit
* tags:
* - teams
* responses:
@@ -50,6 +52,7 @@ import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team";
* 401:
* description: Authorization information is missing or invalid.
* delete:
* operationId: removeTeamById
* summary: Remove an existing team
* parameters:
* - in: path
@@ -57,7 +60,7 @@ import { schemaTeamBodyParams, schemaTeamPublic } from "@lib/validations/team";
* schema:
* type: integer
* required: true
* description: Numeric ID of the team to delete
* description: ID of the team to delete
* tags:
* - teams
* responses:
+2
View File
@@ -16,6 +16,7 @@ async function createOrlistAllTeams(
* @swagger
* /teams:
* get:
* operationId: listTeams
* summary: Find all teams
* tags:
* - teams
@@ -44,6 +45,7 @@ async function createOrlistAllTeams(
* @swagger
* /teams:
* post:
* operationId: addTeam
* summary: Creates a new team
* tags:
* - teams
+108 -68
View File
@@ -10,68 +10,10 @@ import {
} from "@lib/validations/shared/queryIdTransformParseInt";
import { schemaUserEditBodyParams, schemaUserReadPublic } from "@lib/validations/user";
/**
* @swagger
* /users/{id}:
* get:
* summary: Find a user, returns your user if regular user.
* operationId: getUserById
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the user to get
* tags:
* - users
* responses:
* 200:
* description: OK
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: User was not found
* patch:
* summary: Edit an existing user
* operationId: editUserById
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the user to edit
* tags:
* - users
* responses:
* 201:
* description: OK, user edited successfuly
* 400:
* description: Bad request. User body is invalid.
* 401:
* description: Authorization information is missing or invalid.
* delete:
* summary: Remove an existing user
* operationId: deleteUserById
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the user to delete
* tags:
* - users
* responses:
* 201:
* description: OK, user removed successfuly
* 400:
* description: Bad request. User id is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
export async function userById({ method, query, body, userId }: NextApiRequest, res: NextApiResponse<any>) {
export async function userById(
{ method, query, body, userId }: NextApiRequest,
res: NextApiResponse<UserResponse>
) {
const safeQuery = schemaQueryIdParseInt.safeParse(query);
console.log(body);
if (!safeQuery.success) throw new Error("Invalid request query", safeQuery.error);
@@ -79,6 +21,31 @@ export async function userById({ method, query, body, userId }: NextApiRequest,
else {
switch (method) {
case "GET":
/**
* @swagger
* /users/{id}:
* get:
* summary: Find a user, returns your user if regular user.
* operationId: getUserById
* parameters:
* - in: path
* name: id
* example: 4
* schema:
* type: integer
* required: true
* description: ID of the user to get
* tags:
* - users
* responses:
* 200:
* description: OK
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: User was not found
*/
await prisma.user
.findUnique({ where: { id: safeQuery.data.id } })
.then((data) => schemaUserReadPublic.parse(data))
@@ -87,24 +54,72 @@ export async function userById({ method, query, body, userId }: NextApiRequest,
res.status(404).json({ message: `User with id: ${safeQuery.data.id} not found`, error })
);
break;
case "PATCH":
/**
* @swagger
* /users/{id}:
* patch:
* summary: Edit an existing user
* operationId: editUserById
* requestBody:
* description: Edit an existing attendee related to one of your bookings
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* weekStart:
* type: string
* enum: [Monday, Sunday, Saturday]
* example: Monday
* brandColor:
* type: string
* example: "#FF000F"
* darkBrandColor:
* type: string
* example: "#000000"
* timeZone:
* type: string
* example: Europe/London
* parameters:
* - in: path
* name: id
* example: 4
* schema:
* type: integer
* required: true
* description: ID of the user to edit
* tags:
* - users
* responses:
* 201:
* description: OK, user edited successfuly
* 400:
* description: Bad request. User body is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
const safeBody = schemaUserEditBodyParams.safeParse(body);
if (!safeBody.success) {
res.status(400).json({ message: "Bad request", error: safeBody.error });
throw new Error("Invalid request body");
// throw new Error("Invalid request body");
return;
}
const userSchedules = await prisma.schedule.findMany({
where: { userId },
});
const userSchedulesIds = userSchedules.map((schedule) => schedule.id);
// @note: here we make sure user can only make as default his own scheudles
if (!userSchedulesIds.includes(Number(safeBody?.data?.defaultScheduleId))) {
if (
safeBody?.data?.defaultScheduleId &&
!userSchedulesIds.includes(Number(safeBody?.data?.defaultScheduleId))
) {
res.status(400).json({
message: "Bad request",
error: "Invalid default schedule id",
message: "Bad request: Invalid default schedule id",
});
throw new Error("Invalid request body value: defaultScheduleId");
return;
}
await prisma.user
.update({
@@ -118,6 +133,31 @@ export async function userById({ method, query, body, userId }: NextApiRequest,
);
break;
/**
* @swagger
* /users/{id}:
* delete:
* summary: Remove an existing user
* operationId: removeUserById
* parameters:
* - in: path
* name: id
* example: 1
* schema:
* type: integer
* required: true
* description: ID of the user to delete
* tags:
* - users
* responses:
* 201:
* description: OK, user removed successfuly
* 400:
* description: Bad request. User id is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
case "DELETE":
await prisma.user
.delete({ where: { id: safeQuery.data.id } })
+32 -16
View File
@@ -3,17 +3,17 @@ import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { withMiddleware } from "@lib/helpers/withMiddleware";
import { UsersResponse } from "@lib/types";
import { UserResponse, UsersResponse } from "@lib/types";
import { schemaUserReadPublic } from "@lib/validations/user";
/**
* @swagger
* /users:
* get:
* operationId: listUsers
* summary: Find all users.
* tags:
* - users
* operationId: listUsers
* responses:
* 200:
* description: OK
@@ -22,21 +22,37 @@ import { schemaUserReadPublic } from "@lib/validations/user";
* 404:
* description: No users were found
*/
async function allUsers({ userId }: NextApiRequest, res: NextApiResponse<UsersResponse>) {
const data = await prisma.user.findMany({
where: {
id: userId,
},
});
const users = data.map((user) => schemaUserReadPublic.parse(user));
if (users) res.status(200).json({ users });
else
(error: Error) =>
res.status(404).json({
message: "No Users were found",
error,
async function getAllorCreateUser(
{ userId, method, body }: NextApiRequest,
res: NextApiResponse<UsersResponse | UserResponse>
) {
if (method === "GET") {
const data = await prisma.user.findMany({
where: {
id: userId,
},
});
const users = data.map((user) => schemaUserReadPublic.parse(user));
if (users) res.status(200).json({ users });
else
(error: Error) =>
res.status(404).json({
message: "No Users were found",
error,
});
} else if (method === "POST") {
const isAdmin = await prisma.user
.findUnique({ where: { id: userId } })
.then((user) => user?.role === "ADMIN");
if (!isAdmin) res.status(401).json({ message: "You are not authorized" });
else {
const user = await prisma.user.create({
data: schemaUserReadPublic.parse(body),
});
res.status(201).json({ user });
}
}
}
// No POST endpoint for users for now as a regular user you're expected to signup.
export default withMiddleware("HTTP_GET")(allUsers);
export default withMiddleware("HTTP_GET_OR_POST")(getAllorCreateUser);
+1 -1
View File
@@ -20,7 +20,7 @@ import {
* schema:
* type: integer
* required: true
* description: Numeric ID of the resource to delete
* description: ID of the resource to delete
* tags:
* - resources
+1 -1
View File
@@ -21,7 +21,7 @@ import {
* schema:
* type: integer
* required: true
* description: Numeric ID of the resource to edit
* description: ID of the resource to edit
* tags:
* - resources
+1 -1
View File
@@ -21,7 +21,7 @@ import {
* schema:
* type: integer
* required: true
* description: Numeric ID of the resource to get
* description: ID of the resource to get
* tags:
* - resources
+1 -1
View File
@@ -15,5 +15,5 @@
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules", "templates"]
"exclude": ["node_modules", "templates", "auth"]
}