From f63aa5d55059629e740713477f0283e746277816 Mon Sep 17 00:00:00 2001 From: Alex Johansson Date: Thu, 19 Aug 2021 14:27:01 +0200 Subject: [PATCH] add linting in CI + fix lint errors (#473) * run `yarn lint --fix` * Revert "Revert "add linting to ci"" This reverts commit 0bbbbee4bea820343063cc407e13ac31b7f40ef7. * Fixed some errors * remove unused code - not sure why this was here? * assert env var * more type fixes * fix typings og gcal callback - needs testing * rename `md5.ts` to `md5.js` it is js. * fix types * fix types * fix lint errors * fix last lint error Co-authored-by: Alex van Andel --- .github/workflows/lint.yml | 21 ++ components/ActiveLink.tsx | 22 +-- components/Avatar.tsx | 51 ++--- components/ImageUploader.tsx | 162 +++++++--------- components/Slider.tsx | 22 +-- components/ui/Dropdown.tsx | 21 +- components/ui/alerts/Error.tsx | 11 +- lib/clock.ts | 43 +++-- lib/emails/invitation.ts | 66 ++++--- lib/event.ts | 4 +- lib/integrations.ts | 36 ++-- lib/{md5.ts => md5.js} | 23 +-- lib/serverConfig.ts | 64 +++---- lib/telemetry.ts | 133 ++++++------- pages/api/auth/changepw.ts | 68 ++++--- pages/api/auth/signup.ts | 43 ++--- pages/api/availability/calendar.ts | 111 +++++------ pages/api/availability/day.ts | 42 ++-- pages/api/integrations.ts | 62 +++--- pages/api/integrations/googlecalendar/add.ts | 65 +++---- .../integrations/googlecalendar/callback.ts | 48 ++--- .../api/integrations/office365calendar/add.ts | 53 +++-- .../office365calendar/callback.ts | 73 ++++--- pages/api/integrations/zoomvideo/add.ts | 45 +++-- pages/api/integrations/zoomvideo/callback.ts | 59 +++--- pages/api/teams/[team]/index.ts | 21 +- pages/api/teams/[team]/invite.ts | 66 +++---- pages/api/teams/[team]/membership.ts | 47 +++-- pages/api/user/membership.ts | 39 ++-- pages/event-types/[type].tsx | 181 +++++++++--------- pages/reschedule/[uid].tsx | 59 +++--- pages/settings/profile.tsx | 61 +++--- postcss.config.js | 2 +- test/lib/prisma.test.ts | 33 +--- test/lib/slots.test.ts | 74 +++---- tsconfig.json | 16 +- 36 files changed, 988 insertions(+), 959 deletions(-) create mode 100644 .github/workflows/lint.yml rename lib/{md5.ts => md5.js} (91%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..5f71820751 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint +on: [push] +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + version: 14.x + + - name: Install deps + uses: bahmutov/npm-install@v1 + + - name: Lint + run: yarn lint diff --git a/components/ActiveLink.tsx b/components/ActiveLink.tsx index 67b3c1d641..c13c7093b3 100644 --- a/components/ActiveLink.tsx +++ b/components/ActiveLink.tsx @@ -1,22 +1,22 @@ -import {useRouter} from 'next/router' -import Link from 'next/link' -import React, {Children} from 'react' +import { useRouter } from "next/router"; +import Link from "next/link"; +import React, { Children } from "react"; const ActiveLink = ({ children, activeClassName, ...props }) => { - const { asPath } = useRouter() - const child = Children.only(children) - const childClassName = child.props.className || '' + const { asPath } = useRouter(); + const child = Children.only(children); + const childClassName = child.props.className || ""; const className = asPath === props.href || asPath === props.as ? `${childClassName} ${activeClassName}`.trim() - : childClassName + : childClassName; return {React.cloneElement(child, { className })}; -} +}; ActiveLink.defaultProps = { - activeClassName: 'active' -} as Partial + activeClassName: "active", +} as Partial; -export default ActiveLink \ No newline at end of file +export default ActiveLink; diff --git a/components/Avatar.tsx b/components/Avatar.tsx index ce13d974e3..02cf38a9a0 100644 --- a/components/Avatar.tsx +++ b/components/Avatar.tsx @@ -1,32 +1,37 @@ import { useState } from "react"; -import md5 from '../lib/md5'; +import md5 from "../lib/md5"; -export default function Avatar({ user, className = '', fallback, imageSrc = '' }: { - user: any; - className?: string; - fallback?: JSX.Element; - imageSrc?: string; +export default function Avatar({ + user, + className = "", + fallback, + imageSrc = "", +}: { + user: any; + className?: string; + fallback?: JSX.Element; + imageSrc?: string; }) { - const [gravatarAvailable, setGravatarAvailable] = useState(true); + const [gravatarAvailable, setGravatarAvailable] = useState(true); - if (imageSrc) { - return Avatar; - } + if (imageSrc) { + return Avatar; + } - if (user.avatar) { + if (user.avatar) { return Avatar; - } + } - if (gravatarAvailable) { - return ( - setGravatarAvailable(false)} - src={`https://www.gravatar.com/avatar/${md5(user.email)}?s=160&d=identicon&r=PG`} - alt="Avatar" - className={className} - /> - ); - } + if (gravatarAvailable) { + return ( + setGravatarAvailable(false)} + src={`https://www.gravatar.com/avatar/${md5(user.email)}?s=160&d=identicon&r=PG`} + alt="Avatar" + className={className} + /> + ); + } - return fallback || null; + return fallback || null; } diff --git a/components/ImageUploader.tsx b/components/ImageUploader.tsx index d348e1b62d..6b3c43015f 100644 --- a/components/ImageUploader.tsx +++ b/components/ImageUploader.tsx @@ -2,7 +2,7 @@ import Cropper from "react-easy-crop"; import { useState, useCallback, useRef } from "react"; import Slider from "./Slider"; -export default function ImageUploader({target, id, buttonMsg, handleAvatarChange, imageRef}){ +export default function ImageUploader({ target, id, buttonMsg, handleAvatarChange, imageRef }) { const imageFileRef = useRef(); const [imageDataUrl, setImageDataUrl] = useState(); const [croppedAreaPixels, setCroppedAreaPixels] = useState(); @@ -16,9 +16,9 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange const openUploaderModal = () => { imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false); - setImageUploadModalOpen(!imageUploadModalOpen) - } - + setImageUploadModalOpen(!imageUploadModalOpen); + }; + const closeImageUploadModal = () => { setImageUploadModalOpen(false); }; @@ -32,34 +32,33 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange const readFile = (file) => { return new Promise((resolve) => { const reader = new FileReader(); - reader.addEventListener('load', () => resolve(reader.result), false); - reader.readAsDataURL(file) - }) - } + reader.addEventListener("load", () => resolve(reader.result), false); + reader.readAsDataURL(file); + }); + }; const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { setCroppedAreaPixels(croppedAreaPixels); - - }, []) - + }, []); + const CropHandler = () => { setCrop({ x: 0, y: 0 }); setZoom(1); setImageLoaded(true); - } + }; const handleZoomSliderChange = ([value]) => { value < 1 ? setZoom(1) : setZoom(value); - } + }; const createImage = (url) => - new Promise((resolve, reject) => { - const image = new Image(); - image.addEventListener('load', () => resolve(image)); - image.addEventListener('error', error => reject(error)); - image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox - image.src = url; - }) + new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener("load", () => resolve(image)); + image.addEventListener("error", (error) => reject(error)); + image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox + image.src = url; + }); function getRadianAngle(degreeValue) { return (degreeValue * Math.PI) / 180; @@ -67,75 +66,64 @@ export default function ImageUploader({target, id, buttonMsg, handleAvatarChange async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) { const image = await createImage(imageSrc); - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const maxSize = Math.max(image.width, image.height); const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); - + // set each dimensions to double largest dimension to allow for a safe area for the // image to rotate in without being clipped by canvas context canvas.width = safeArea; canvas.height = safeArea; - + // translate canvas context to a central location on image to allow rotating around the center. ctx.translate(safeArea / 2, safeArea / 2); ctx.rotate(getRadianAngle(rotation)); ctx.translate(-safeArea / 2, -safeArea / 2); - + // draw rotated image and store data. - ctx.drawImage( - image, - safeArea / 2 - image.width * 0.5, - safeArea / 2 - image.height * 0.5 - ); + ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5); const data = ctx.getImageData(0, 0, safeArea, safeArea); - + // set canvas width to final desired crop size - this will clear existing context canvas.width = pixelCrop.width; canvas.height = pixelCrop.height; - + // paste generated rotate image with correct offsets for x,y crop values. ctx.putImageData( data, Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) ); - - // As Base64 string - return canvas.toDataURL('image/jpeg'); - } + // As Base64 string + return canvas.toDataURL("image/jpeg"); + } const showCroppedImage = useCallback(async () => { try { - const croppedImage = await getCroppedImg( - imageDataUrl, - croppedAreaPixels, - rotation - ) - setIsImageShown(true) - setShownImage(croppedImage) - setImageLoaded(false) - handleAvatarChange(croppedImage) - closeImageUploadModal() + const croppedImage = await getCroppedImg(imageDataUrl, croppedAreaPixels, rotation); + setIsImageShown(true); + setShownImage(croppedImage); + setImageLoaded(false); + handleAvatarChange(croppedImage); + closeImageUploadModal(); } catch (e) { - console.error(e) + console.error(e); } }, [croppedAreaPixels, rotation]); return (
- { - imageUploadModalOpen && + {imageUploadModalOpen && ( - + - +
-
- {!imageLoaded && + {!imageLoaded && (
- {!isImageShown && -

No {target}

- } - {isImageShown && - {target} - } + {!isImageShown && ( +

No {target}

+ )} + {isImageShown && ( + {target} + )}
- } - {imageLoaded && + )} + {imageLoaded && (
-
- +
- } - - + Choose a file... + + Save -
- - } + )}
- - ) -} \ No newline at end of file + ); +} diff --git a/components/Slider.tsx b/components/Slider.tsx index 4e359a4bc6..564c74c228 100644 --- a/components/Slider.tsx +++ b/components/Slider.tsx @@ -1,22 +1,20 @@ -import React from 'react'; -import * as SliderPrimitive from '@radix-ui/react-slider'; +import React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; -const Slider = ({value, min, max, step, label, changeHandler}) => ( - ( + - - - - + onValueChange={changeHandler}> + + + + ); export default Slider; - diff --git a/components/ui/Dropdown.tsx b/components/ui/Dropdown.tsx index 7cf8c97e0f..777c161e2c 100644 --- a/components/ui/Dropdown.tsx +++ b/components/ui/Dropdown.tsx @@ -1,19 +1,20 @@ -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; export default function Dropdown(props) { + const [open, setOpen] = useState(false); - const [ open, setOpen ] = useState(false); - - useEffect( () => { - document.addEventListener('keyup', (e) => { + useEffect(() => { + document.addEventListener("keyup", (e) => { if (e.key === "Escape") { setOpen(false); } }); }, [open]); - return (
setOpen(!open)} {...props}> - {props.children[0]} - {open && props.children[1]} -
); -} \ No newline at end of file + return ( +
setOpen(!open)} {...props}> + {props.children[0]} + {open && props.children[1]} +
+ ); +} diff --git a/components/ui/alerts/Error.tsx b/components/ui/alerts/Error.tsx index 8cc239988d..19b0e2ce49 100644 --- a/components/ui/alerts/Error.tsx +++ b/components/ui/alerts/Error.tsx @@ -1,5 +1,4 @@ - -import { XCircleIcon } from '@heroicons/react/solid' +import { XCircleIcon } from "@heroicons/react/solid"; export default function ErrorAlert(props) { return ( @@ -11,12 +10,10 @@ export default function ErrorAlert(props) {

Something went wrong

-

- {props.message} -

+

{props.message}

- ) -} \ No newline at end of file + ); +} diff --git a/lib/clock.ts b/lib/clock.ts index f082d9db1d..73251a184d 100644 --- a/lib/clock.ts +++ b/lib/clock.ts @@ -1,48 +1,51 @@ // handles logic related to user clock display using 24h display / timeZone options. -import dayjs, {Dayjs} from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import timezone from 'dayjs/plugin/timezone'; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; -dayjs.extend(utc) -dayjs.extend(timezone) +dayjs.extend(utc); +dayjs.extend(timezone); -interface TimeOptions { is24hClock: boolean, inviteeTimeZone: string }; +interface TimeOptions { + is24hClock: boolean; + inviteeTimeZone: string; +} const timeOptions: TimeOptions = { is24hClock: false, - inviteeTimeZone: '', -} + inviteeTimeZone: "", +}; -const isInitialized: boolean = false; +const isInitialized = false; const initClock = () => { if (typeof localStorage === "undefined" || isInitialized) { return; } - timeOptions.is24hClock = localStorage.getItem('timeOption.is24hClock') === "true"; - timeOptions.inviteeTimeZone = localStorage.getItem('timeOption.preferredTimeZone') || dayjs.tz.guess(); -} + timeOptions.is24hClock = localStorage.getItem("timeOption.is24hClock") === "true"; + timeOptions.inviteeTimeZone = localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess(); +}; const is24h = (is24hClock?: boolean) => { initClock(); - if(typeof is24hClock !== "undefined") set24hClock(is24hClock); + if (typeof is24hClock !== "undefined") set24hClock(is24hClock); return timeOptions.is24hClock; -} +}; const set24hClock = (is24hClock: boolean) => { - localStorage.setItem('timeOption.is24hClock', is24hClock.toString()); + localStorage.setItem("timeOption.is24hClock", is24hClock.toString()); timeOptions.is24hClock = is24hClock; -} +}; function setTimeZone(selectedTimeZone: string) { - localStorage.setItem('timeOption.preferredTimeZone', selectedTimeZone); + localStorage.setItem("timeOption.preferredTimeZone", selectedTimeZone); timeOptions.inviteeTimeZone = selectedTimeZone; } const timeZone = (selectedTimeZone?: string) => { initClock(); - if (selectedTimeZone) setTimeZone(selectedTimeZone) + if (selectedTimeZone) setTimeZone(selectedTimeZone); return timeOptions.inviteeTimeZone; -} +}; -export {is24h, timeZone}; \ No newline at end of file +export { is24h, timeZone }; diff --git a/lib/emails/invitation.ts b/lib/emails/invitation.ts index 542880c5ef..ee4668657e 100644 --- a/lib/emails/invitation.ts +++ b/lib/emails/invitation.ts @@ -1,6 +1,5 @@ - -import {serverConfig} from "../serverConfig"; -import nodemailer from 'nodemailer'; +import { serverConfig } from "../serverConfig"; +import nodemailer from "nodemailer"; export default function createInvitationEmail(data: any, options: any = {}) { return sendEmail(data, { @@ -8,33 +7,33 @@ export default function createInvitationEmail(data: any, options: any = {}) { transport: serverConfig.transport, from: serverConfig.from, }, - ...options + ...options, }); } -const sendEmail = (invitation: any, { - provider, -}) => new Promise( (resolve, reject) => { - const { transport, from } = provider; +const sendEmail = (invitation: any, { provider }) => + new Promise((resolve, reject) => { + const { transport, from } = provider; - nodemailer.createTransport(transport).sendMail( - { - from: `Calendso <${from}>`, - to: invitation.toEmail, - subject: ( - invitation.from ? invitation.from + ' invited you' : 'You have been invited' - ) + ` to join ${invitation.teamName}`, - html: html(invitation), - text: text(invitation), - }, - (error) => { - if (error) { - console.error("SEND_INVITATION_NOTIFICATION_ERROR", invitation.toEmail, error); - return reject(new Error(error)); + nodemailer.createTransport(transport).sendMail( + { + from: `Calendso <${from}>`, + to: invitation.toEmail, + subject: + (invitation.from ? invitation.from + " invited you" : "You have been invited") + + ` to join ${invitation.teamName}`, + html: html(invitation), + text: text(invitation), + }, + (error) => { + if (error) { + console.error("SEND_INVITATION_NOTIFICATION_ERROR", invitation.toEmail, error); + return reject(new Error(error)); + } + return resolve(); } - return resolve(); - }); -}); + ); + }); const html = (invitation: any) => { let url: string = process.env.BASE_URL + "/settings/teams"; @@ -42,7 +41,8 @@ const html = (invitation: any) => { url = `${process.env.BASE_URL}/auth/signup?token=${invitation.token}&callbackUrl=${url}`; } - return ` + return ( + `
@@ -52,8 +52,8 @@ const html = (invitation: any) => { Hi,

` + - (invitation.from ? invitation.from + ' invited you' : 'You have been invited' ) - + ` to join the team "${invitation.teamName}" in Calendso.
+ (invitation.from ? invitation.from + " invited you" : "You have been invited") + + ` to join the team "${invitation.teamName}" in Calendso.

@@ -79,8 +79,12 @@ const html = (invitation: any) => {
- `; -} + ` + ); +}; // just strip all HTML and convert
to \n -const text = (evt: any) => html(evt).replace('
', "\n").replace(/<[^>]+>/g, ''); \ No newline at end of file +const text = (evt: any) => + html(evt) + .replace("
", "\n") + .replace(/<[^>]+>/g, ""); diff --git a/lib/event.ts b/lib/event.ts index 829ee7e1e1..30cb201c87 100644 --- a/lib/event.ts +++ b/lib/event.ts @@ -1,5 +1,3 @@ export function getEventName(name: string, eventTitle: string, eventNameTemplate?: string) { - return eventNameTemplate - ? eventNameTemplate.replace("{USER}", name) - : eventTitle + ' with ' + name + return eventNameTemplate ? eventNameTemplate.replace("{USER}", name) : eventTitle + " with " + name; } diff --git a/lib/integrations.ts b/lib/integrations.ts index c134bf94d0..76e96e554f 100644 --- a/lib/integrations.ts +++ b/lib/integrations.ts @@ -1,21 +1,21 @@ -export function getIntegrationName(name: String) { - switch(name) { - case "google_calendar": - return "Google Calendar"; - case "office365_calendar": - return "Office 365 Calendar"; - case "zoom_video": - return "Zoom"; - case "caldav_calendar": - return "CalDav Server"; - default: - return "Unknown"; - } +export function getIntegrationName(name: string) { + switch (name) { + case "google_calendar": + return "Google Calendar"; + case "office365_calendar": + return "Office 365 Calendar"; + case "zoom_video": + return "Zoom"; + case "caldav_calendar": + return "CalDav Server"; + default: + return "Unknown"; + } } -export function getIntegrationType(name: String) { - if (name.endsWith('_calendar')) { - return 'Calendar'; - } - return "Unknown"; +export function getIntegrationType(name: string) { + if (name.endsWith("_calendar")) { + return "Calendar"; + } + return "Unknown"; } diff --git a/lib/md5.ts b/lib/md5.js similarity index 91% rename from lib/md5.ts rename to lib/md5.js index b16f9caedc..67f06c64ef 100644 --- a/lib/md5.ts +++ b/lib/md5.js @@ -1,5 +1,5 @@ function md5cycle(x, k) { - var a = x[0], + let a = x[0], b = x[1], c = x[2], d = x[3]; @@ -100,17 +100,15 @@ function ii(a, b, c, d, x, s, t) { } function md51(s) { - let txt = ""; - var n = s.length, + let n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= s.length; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } s = s.substring(i - 64); - var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for (i = 0; i < s.length; i++) - tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3); + const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3); tail[i >> 2] |= 0x80 << (i % 4 << 3); if (i > 55) { md5cycle(state, tail); @@ -138,7 +136,7 @@ function md51(s) { */ function md5blk(s) { /* I figured global was faster. */ - var md5blks = [], + let md5blks = [], i; /* Andy King said do it this way. */ for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = @@ -150,18 +148,17 @@ function md5blk(s) { return md5blks; } -var hex_chr = "0123456789abcdef".split(""); +const hex_chr = "0123456789abcdef".split(""); function rhex(n) { - var s = "", + let s = "", j = 0; - for (; j < 4; j++) - s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; + for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; return s; } function hex(x) { - for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); + for (let i = 0; i < x.length; i++) x[i] = rhex(x[i]); return x.join(""); } @@ -173,4 +170,4 @@ function add32(a, b) { return (a + b) & 0xffffffff; } -export default md5; \ No newline at end of file +export default md5; diff --git a/lib/serverConfig.ts b/lib/serverConfig.ts index 2676193cca..9b3a028696 100644 --- a/lib/serverConfig.ts +++ b/lib/serverConfig.ts @@ -1,33 +1,31 @@ - -function detectTransport(): string | any { - - if (process.env.EMAIL_SERVER) { - return process.env.EMAIL_SERVER; - } - - if (process.env.EMAIL_SERVER_HOST) { - const port = parseInt(process.env.EMAIL_SERVER_PORT); - const transport = { - host: process.env.EMAIL_SERVER_HOST, - port, - auth: { - user: process.env.EMAIL_SERVER_USER, - pass: process.env.EMAIL_SERVER_PASSWORD, - }, - secure: (port === 465), - }; - - return transport; - } - - return { - sendmail: true, - newline: 'unix', - path: '/usr/sbin/sendmail' - }; -} - -export const serverConfig = { - transport: detectTransport(), - from: process.env.EMAIL_FROM, -}; \ No newline at end of file +function detectTransport(): string | any { + if (process.env.EMAIL_SERVER) { + return process.env.EMAIL_SERVER; + } + + if (process.env.EMAIL_SERVER_HOST) { + const port = parseInt(process.env.EMAIL_SERVER_PORT); + const transport = { + host: process.env.EMAIL_SERVER_HOST, + port, + auth: { + user: process.env.EMAIL_SERVER_USER, + pass: process.env.EMAIL_SERVER_PASSWORD, + }, + secure: port === 465, + }; + + return transport; + } + + return { + sendmail: true, + newline: "unix", + path: "/usr/sbin/sendmail", + }; +} + +export const serverConfig = { + transport: detectTransport(), + from: process.env.EMAIL_FROM, +}; diff --git a/lib/telemetry.ts b/lib/telemetry.ts index 432a79ae20..61e83d4415 100644 --- a/lib/telemetry.ts +++ b/lib/telemetry.ts @@ -1,39 +1,43 @@ -import React, {useContext} from 'react' -import {jitsuClient, JitsuClient} from "@jitsu/sdk-js"; +import React, { useContext } from "react"; +import { jitsuClient, JitsuClient } from "@jitsu/sdk-js"; /** * Enumeration of all event types that are being sent * to telemetry collection. */ export const telemetryEventTypes = { - pageView: 'page_view', - dateSelected: 'date_selected', - timeSelected: 'time_selected', - bookingConfirmed: 'booking_confirmed', - bookingCancelled: 'booking_cancelled' -} + pageView: "page_view", + dateSelected: "date_selected", + timeSelected: "time_selected", + bookingConfirmed: "booking_confirmed", + bookingCancelled: "booking_cancelled", +}; /** * Telemetry client */ export type TelemetryClient = { - /** - * Use it as: withJitsu((jitsu) => {return jitsu.track()}). If telemetry is disabled, the callback will ignored - * - * ATTENTION: always return the value of jitsu.track() or id() call. Otherwise unhandled rejection can happen, - * which is handled in Next.js with a popup. - */ - withJitsu: (callback: (jitsu: JitsuClient) => void | Promise) => void -} + /** + * Use it as: withJitsu((jitsu) => {return jitsu.track()}). If telemetry is disabled, the callback will ignored + * + * ATTENTION: always return the value of jitsu.track() or id() call. Otherwise unhandled rejection can happen, + * which is handled in Next.js with a popup. + */ + withJitsu: (callback: (jitsu: JitsuClient) => void | Promise) => void; +}; -const emptyClient: TelemetryClient = {withJitsu: () => {}}; +const emptyClient: TelemetryClient = { + withJitsu: () => { + // empty + }, +}; function useTelemetry(): TelemetryClient { - return useContext(TelemetryContext); + return useContext(TelemetryContext); } function isLocalhost(host: string) { - return "localhost" === host || "127.0.0.1" === host; + return "localhost" === host || "127.0.0.1" === host; } /** @@ -41,58 +45,57 @@ function isLocalhost(host: string) { * @param route current next.js route */ export function collectPageParameters(route?: string): any { - let host = document.location.hostname; - let maskedHost = isLocalhost(host) ? "localhost" : "masked"; - //starts with '' - let docPath = route ?? ""; - return { - page_url: route, - page_title: "", - source_ip: "", - url: document.location.protocol + "//" + host + (docPath ?? ""), - doc_host: maskedHost, - doc_search: "", - doc_path: docPath, - referer: "", - } + const host = document.location.hostname; + const maskedHost = isLocalhost(host) ? "localhost" : "masked"; + //starts with '' + const docPath = route ?? ""; + return { + page_url: route, + page_title: "", + source_ip: "", + url: document.location.protocol + "//" + host + (docPath ?? ""), + doc_host: maskedHost, + doc_search: "", + doc_path: docPath, + referer: "", + }; } function createTelemetryClient(): TelemetryClient { - if (process.env.NEXT_PUBLIC_TELEMETRY_KEY) { - return { - withJitsu: (callback) => { - if (!process.env.NEXT_PUBLIC_TELEMETRY_KEY) { - //telemetry is disabled - return; - } - if (!window) { - console.warn("Jitsu has been called during SSR, this scenario isn't supported yet"); - return; - } else if (!window['jitsu']) { - window['jitsu'] = jitsuClient({ - log_level: 'ERROR', - tracking_host: "https://t.calendso.com", - key: process.env.NEXT_PUBLIC_TELEMETRY_KEY, - cookie_name: "__clnds", - capture_3rd_party_cookies: false, - }); - } - let res = callback(window['jitsu']); - if (res && typeof res['catch'] === "function") { - res.catch(e => { - console.debug("Unable to send telemetry event", e) - }); - } - } + if (process.env.NEXT_PUBLIC_TELEMETRY_KEY) { + return { + withJitsu: (callback) => { + if (!process.env.NEXT_PUBLIC_TELEMETRY_KEY) { + //telemetry is disabled + return; } - } else { - return emptyClient; - } + if (!window) { + console.warn("Jitsu has been called during SSR, this scenario isn't supported yet"); + return; + } else if (!window["jitsu"]) { + window["jitsu"] = jitsuClient({ + log_level: "ERROR", + tracking_host: "https://t.calendso.com", + key: process.env.NEXT_PUBLIC_TELEMETRY_KEY, + cookie_name: "__clnds", + capture_3rd_party_cookies: false, + }); + } + const res = callback(window["jitsu"]); + if (res && typeof res["catch"] === "function") { + res.catch((e) => { + console.debug("Unable to send telemetry event", e); + }); + } + }, + }; + } else { + return emptyClient; + } } +const TelemetryContext = React.createContext(emptyClient); -const TelemetryContext = React.createContext(emptyClient) - -const TelemetryProvider = TelemetryContext.Provider +const TelemetryProvider = TelemetryContext.Provider; export { TelemetryContext, TelemetryProvider, createTelemetryClient, useTelemetry }; diff --git a/pages/api/auth/changepw.ts b/pages/api/auth/changepw.ts index ba4fb282f7..f26c8427ae 100644 --- a/pages/api/auth/changepw.ts +++ b/pages/api/auth/changepw.ts @@ -4,43 +4,49 @@ import { getSession } from "@lib/auth"; import prisma from "../../../lib/prisma"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({req: req}); + const session = await getSession({ req: req }); - if (!session) { - res.status(401).json({message: "Not authenticated"}); - return; - } + if (!session) { + res.status(401).json({ message: "Not authenticated" }); + return; + } - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true, - password: true - } - }); + const user = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + password: true, + }, + }); - if (!user) { res.status(404).json({message: 'User not found'}); return; } + if (!user) { + res.status(404).json({ message: "User not found" }); + return; + } - const oldPassword = req.body.oldPassword; - const newPassword = req.body.newPassword; - const currentPassword = user.password; + const oldPassword = req.body.oldPassword; + const newPassword = req.body.newPassword; + const currentPassword = user.password; - const passwordsMatch = await verifyPassword(oldPassword, currentPassword); + const passwordsMatch = await verifyPassword(oldPassword, currentPassword); - if (!passwordsMatch) { res.status(403).json({message: 'Incorrect password'}); return; } + if (!passwordsMatch) { + res.status(403).json({ message: "Incorrect password" }); + return; + } - const hashedPassword = await hashPassword(newPassword); + const hashedPassword = await hashPassword(newPassword); - const updateUser = await prisma.user.update({ - where: { - id: user.id, - }, - data: { - password: hashedPassword, - }, - }); + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + password: hashedPassword, + }, + }); - res.status(200).json({message: 'Password updated successfully'}); -} \ No newline at end of file + res.status(200).json({ message: "Password updated successfully" }); +} diff --git a/pages/api/auth/signup.ts b/pages/api/auth/signup.ts index 9d11f38f26..c8afa8137c 100644 --- a/pages/api/auth/signup.ts +++ b/pages/api/auth/signup.ts @@ -1,26 +1,26 @@ -import prisma from '../../../lib/prisma'; +import prisma from "../../../lib/prisma"; import { hashPassword } from "../../../lib/auth"; export default async function handler(req, res) { - if (req.method !== 'POST') { - return; + if (req.method !== "POST") { + return; } const data = req.body; const { username, email, password } = data; if (!username) { - res.status(422).json({message: 'Invalid username'}); + res.status(422).json({ message: "Invalid username" }); return; } - if (!email || !email.includes('@')) { - res.status(422).json({message: 'Invalid email'}); + if (!email || !email.includes("@")) { + res.status(422).json({ message: "Invalid email" }); return; } if (!password || password.trim().length < 7) { - res.status(422).json({message: 'Invalid input - password should be at least 7 characters long.'}); + res.status(422).json({ message: "Invalid input - password should be at least 7 characters long." }); return; } @@ -28,34 +28,33 @@ export default async function handler(req, res) { where: { OR: [ { - username: username + username: username, }, { - email: email - } + email: email, + }, ], AND: [ { emailVerified: { not: null, }, - } - ] - } + }, + ], + }, }); if (existingUser) { - let message: string = ( - existingUser.email !== email - ) ? 'Username already taken' : 'Email address is already registered'; + const message: string = + existingUser.email !== email ? "Username already taken" : "Email address is already registered"; - return res.status(409).json({message}); + return res.status(409).json({ message }); } const hashedPassword = await hashPassword(password); - const user = await prisma.user.upsert({ - where: { email, }, + await prisma.user.upsert({ + where: { email }, update: { username, password: hashedPassword, @@ -65,8 +64,8 @@ export default async function handler(req, res) { username, email, password: hashedPassword, - } + }, }); - res.status(201).json({message: 'Created user'}); -} \ No newline at end of file + res.status(201).json({ message: "Created user" }); +} diff --git a/pages/api/availability/calendar.ts b/pages/api/availability/calendar.ts index 83ad9f6217..6b7b8d9ef2 100644 --- a/pages/api/availability/calendar.ts +++ b/pages/api/availability/calendar.ts @@ -4,66 +4,67 @@ import prisma from "../../../lib/prisma"; import { IntegrationCalendar, listCalendars } from "../../../lib/calendarClient"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({req: req}); + const session = await getSession({ req: req }); - if (!session) { - res.status(401).json({message: "Not authenticated"}); - return; - } + if (!session) { + res.status(401).json({ message: "Not authenticated" }); + return; + } - const currentUser = await prisma.user.findFirst({ - where: { - id: session.user.id, + const currentUser = await prisma.user.findFirst({ + where: { + id: session.user.id, + }, + select: { + credentials: true, + timeZone: true, + id: true, + }, + }); + + if (req.method == "POST") { + await prisma.selectedCalendar.create({ + data: { + user: { + connect: { + id: currentUser.id, + }, }, - select: { - credentials: true, - timeZone: true, - id: true - } + integration: req.body.integration, + externalId: req.body.externalId, + }, + }); + res.status(200).json({ message: "Calendar Selection Saved" }); + } + + if (req.method == "DELETE") { + await prisma.selectedCalendar.delete({ + where: { + userId_integration_externalId: { + userId: currentUser.id, + externalId: req.body.externalId, + integration: req.body.integration, + }, + }, }); - if (req.method == "POST") { - await prisma.selectedCalendar.create({ - data: { - user: { - connect: { - id: currentUser.id - } - }, - integration: req.body.integration, - externalId: req.body.externalId - } - }); - res.status(200).json({message: "Calendar Selection Saved"}); + res.status(200).json({ message: "Calendar Selection Saved" }); + } - } + if (req.method == "GET") { + const selectedCalendarIds = await prisma.selectedCalendar.findMany({ + where: { + userId: currentUser.id, + }, + select: { + externalId: true, + }, + }); - if (req.method == "DELETE") { - await prisma.selectedCalendar.delete({ - where: { - userId_integration_externalId: { - userId: currentUser.id, - externalId: req.body.externalId, - integration: req.body.integration - } - } - }); - - res.status(200).json({message: "Calendar Selection Saved"}); - } - - if (req.method == "GET") { - const selectedCalendarIds = await prisma.selectedCalendar.findMany({ - where: { - userId: currentUser.id - }, - select: { - externalId: true - } - }); - - const calendars: IntegrationCalendar[] = await listCalendars(currentUser.credentials); - const selectableCalendars = calendars.map(cal => {return {selected: selectedCalendarIds.findIndex(s => s.externalId === cal.externalId) > -1, ...cal}}); - res.status(200).json(selectableCalendars); - } + const calendars: IntegrationCalendar[] = await listCalendars(currentUser.credentials); + const selectableCalendars = calendars.map((cal) => { + return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal }; + }); + res.status(200).json(selectableCalendars); + } } diff --git a/pages/api/availability/day.ts b/pages/api/availability/day.ts index 57a66998e3..d98dbfe972 100644 --- a/pages/api/availability/day.ts +++ b/pages/api/availability/day.ts @@ -3,29 +3,29 @@ import { getSession } from "@lib/auth"; import prisma from "../../../lib/prisma"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({req: req}); + const session = await getSession({ req: req }); - if (!session) { - res.status(401).json({message: "Not authenticated"}); - return; - } + if (!session) { + res.status(401).json({ message: "Not authenticated" }); + return; + } - if (req.method == "PATCH") { - const startMins = req.body.start; - const endMins = req.body.end; - const bufferMins = req.body.buffer; + if (req.method == "PATCH") { + const startMins = req.body.start; + const endMins = req.body.end; + const bufferMins = req.body.buffer; - const updateDay = await prisma.user.update({ - where: { - id: session.user.id, - }, - data: { - startTime: startMins, - endTime: endMins, - bufferTime: bufferMins - }, - }); + await prisma.user.update({ + where: { + id: session.user.id, + }, + data: { + startTime: startMins, + endTime: endMins, + bufferTime: bufferMins, + }, + }); - res.status(200).json({message: 'Start and end times updated successfully'}); - } + res.status(200).json({ message: "Start and end times updated successfully" }); + } } diff --git a/pages/api/integrations.ts b/pages/api/integrations.ts index 28aa700b73..264ef0c8b1 100644 --- a/pages/api/integrations.ts +++ b/pages/api/integrations.ts @@ -2,38 +2,44 @@ import prisma from "../../lib/prisma"; import { getSession } from "@lib/auth"; export default async function handler(req, res) { - if (req.method === 'GET') { - // Check that user is authenticated - const session = await getSession({req: req}); + if (req.method === "GET") { + // Check that user is authenticated + const session = await getSession({ req: req }); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } - - const credentials = await prisma.credential.findMany({ - where: { - userId: session.user.id, - }, - select: { - type: true, - key: true - } - }); - - res.status(200).json(credentials); + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; } - if (req.method == "DELETE") { - const session = await getSession({req: req}); + const credentials = await prisma.credential.findMany({ + where: { + userId: session.user.id, + }, + select: { + type: true, + key: true, + }, + }); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } + res.status(200).json(credentials); + } - const id = req.body.id; + if (req.method == "DELETE") { + const session = await getSession({ req: req }); - const deleteIntegration = await prisma.credential.delete({ - where: { - id: id, - }, - }); - - res.status(200).json({message: 'Integration deleted successfully'}); + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; } -} \ No newline at end of file + + const id = req.body.id; + + await prisma.credential.delete({ + where: { + id: id, + }, + }); + + res.status(200).json({ message: "Integration deleted successfully" }); + } +} diff --git a/pages/api/integrations/googlecalendar/add.ts b/pages/api/integrations/googlecalendar/add.ts index 762a9016a2..54242a59e2 100644 --- a/pages/api/integrations/googlecalendar/add.ts +++ b/pages/api/integrations/googlecalendar/add.ts @@ -1,42 +1,37 @@ -import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; -import prisma from "../../../../lib/prisma"; -const { google } = require("googleapis"); +import { google } from "googleapis"; +import type { NextApiRequest, NextApiResponse } from "next"; -const credentials = process.env.GOOGLE_API_CREDENTIALS; -const scopes = ['https://www.googleapis.com/auth/calendar.readonly', 'https://www.googleapis.com/auth/calendar.events']; +const credentials = process.env.GOOGLE_API_CREDENTIALS!; +const scopes = [ + "https://www.googleapis.com/auth/calendar.readonly", + "https://www.googleapis.com/auth/calendar.events", +]; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === 'GET') { - // Check that user is authenticated - const session = await getSession({req: req}); + if (req.method === "GET") { + // Check that user is authenticated + const session = await getSession({ req: req }); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } - - // Get user - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true - } - }); - - // Get token from Google Calendar API - const {client_secret, client_id, redirect_uris} = JSON.parse(credentials).web; - const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); - - const authUrl = oAuth2Client.generateAuthUrl({ - access_type: 'offline', - scope: scopes, - // A refresh token is only returned the first time the user - // consents to providing access. For illustration purposes, - // setting the prompt to 'consent' will force this consent - // every time, forcing a refresh_token to be returned. - prompt: 'consent', - }); - - res.status(200).json({url: authUrl}); + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; } + + // Get token from Google Calendar API + const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web; + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: "offline", + scope: scopes, + // A refresh token is only returned the first time the user + // consents to providing access. For illustration purposes, + // setting the prompt to 'consent' will force this consent + // every time, forcing a refresh_token to be returned. + prompt: "consent", + }); + + res.status(200).json({ url: authUrl }); + } } diff --git a/pages/api/integrations/googlecalendar/callback.ts b/pages/api/integrations/googlecalendar/callback.ts index 86699b1983..26fd9b5b42 100644 --- a/pages/api/integrations/googlecalendar/callback.ts +++ b/pages/api/integrations/googlecalendar/callback.ts @@ -1,34 +1,36 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; import prisma from "../../../../lib/prisma"; -const { google } = require("googleapis"); +import { google } from "googleapis"; -const credentials = process.env.GOOGLE_API_CREDENTIALS; +const credentials = process.env.GOOGLE_API_CREDENTIALS!; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { code } = req.query; + const { code } = req.query; - // Check that user is authenticated - const session = await getSession({req: req}); + // Check that user is authenticated + const session = await getSession({ req: req }); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; + } + if (typeof code !== "string") { + res.status(400).json({ message: "`code` must be a string" }); + return; + } - const {client_secret, client_id, redirect_uris} = JSON.parse(credentials).web; - const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web; + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + const token = await oAuth2Client.getToken(code); - // Convert to token - return new Promise( (resolve, reject) => oAuth2Client.getToken(code, async (err, token) => { - if (err) return console.error('Error retrieving access token', err); + await prisma.credential.create({ + data: { + type: "google_calendar", + key: token as any, + userId: session.user.id, + }, + }); - const credential = await prisma.credential.create({ - data: { - type: 'google_calendar', - key: token, - userId: session.user.id - } - }); - - res.redirect('/integrations'); - resolve(); - })); -} \ No newline at end of file + res.redirect("/integrations"); +} diff --git a/pages/api/integrations/office365calendar/add.ts b/pages/api/integrations/office365calendar/add.ts index 7b9059ded6..d23b3275ae 100644 --- a/pages/api/integrations/office365calendar/add.ts +++ b/pages/api/integrations/office365calendar/add.ts @@ -2,29 +2,40 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; import prisma from "../../../../lib/prisma"; -const scopes = ['User.Read', 'Calendars.Read', 'Calendars.ReadWrite', 'offline_access']; +const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"]; + +function generateAuthUrl() { + return ( + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=" + + scopes.join(" ") + + "&client_id=" + + process.env.MS_GRAPH_CLIENT_ID + + "&redirect_uri=" + + process.env.BASE_URL + + "/api/integrations/office365calendar/callback" + ); +} export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === 'GET') { - // Check that user is authenticated - const session = await getSession({req: req}); + if (req.method === "GET") { + // Check that user is authenticated + const session = await getSession({ req: req }); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } - - // Get user - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true - } - }); - - function generateAuthUrl() { - return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code&scope=' + scopes.join(' ') + '&client_id=' + process.env.MS_GRAPH_CLIENT_ID + '&redirect_uri=' + process.env.BASE_URL + '/api/integrations/office365calendar/callback'; - } - - res.status(200).json({url: generateAuthUrl() }); + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; } + + // Get user + await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + }, + }); + + res.status(200).json({ url: generateAuthUrl() }); + } } diff --git a/pages/api/integrations/office365calendar/callback.ts b/pages/api/integrations/office365calendar/callback.ts index 676dcf83d2..a410c214d0 100644 --- a/pages/api/integrations/office365calendar/callback.ts +++ b/pages/api/integrations/office365calendar/callback.ts @@ -4,41 +4,60 @@ import prisma from "../../../../lib/prisma"; const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"]; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { code } = req.query; + const { code } = req.query; - // Check that user is authenticated - const session = await getSession({req: req}); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } + // Check that user is authenticated + const session = await getSession({ req: req }); + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; + } - const toUrlEncoded = payload => Object.keys(payload).map( (key) => key + '=' + encodeURIComponent(payload[ key ]) ).join('&'); + const toUrlEncoded = (payload) => + Object.keys(payload) + .map((key) => key + "=" + encodeURIComponent(payload[key])) + .join("&"); - const body = toUrlEncoded({ client_id: process.env.MS_GRAPH_CLIENT_ID, grant_type: 'authorization_code', code, scope: scopes.join(' '), redirect_uri: process.env.BASE_URL + '/api/integrations/office365calendar/callback', client_secret: process.env.MS_GRAPH_CLIENT_SECRET }); + const body = toUrlEncoded({ + client_id: process.env.MS_GRAPH_CLIENT_ID, + grant_type: "authorization_code", + code, + scope: scopes.join(" "), + redirect_uri: process.env.BASE_URL + "/api/integrations/office365calendar/callback", + client_secret: process.env.MS_GRAPH_CLIENT_SECRET, + }); - const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', - }, body }); + const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + }, + body, + }); - const responseBody = await response.json(); + const responseBody = await response.json(); - if (!response.ok) { - return res.redirect('/integrations?error=' + JSON.stringify(responseBody)); - } + if (!response.ok) { + return res.redirect("/integrations?error=" + JSON.stringify(responseBody)); + } - const whoami = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { 'Authorization': 'Bearer ' + responseBody.access_token } }); - const graphUser = await whoami.json(); + const whoami = await fetch("https://graph.microsoft.com/v1.0/me", { + headers: { Authorization: "Bearer " + responseBody.access_token }, + }); + const graphUser = await whoami.json(); - // In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address. - responseBody.email = graphUser.mail ?? graphUser.userPrincipalName; - responseBody.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in); // set expiry date in seconds - delete responseBody.expires_in; + // In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address. + responseBody.email = graphUser.mail ?? graphUser.userPrincipalName; + responseBody.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); // set expiry date in seconds + delete responseBody.expires_in; - const credential = await prisma.credential.create({ - data: { - type: 'office365_calendar', - key: responseBody, - userId: session.user.id - } - }); + await prisma.credential.create({ + data: { + type: "office365_calendar", + key: responseBody, + userId: session.user.id, + }, + }); - return res.redirect('/integrations'); + return res.redirect("/integrations"); } diff --git a/pages/api/integrations/zoomvideo/add.ts b/pages/api/integrations/zoomvideo/add.ts index 20b1cff73a..08e074ab08 100644 --- a/pages/api/integrations/zoomvideo/add.ts +++ b/pages/api/integrations/zoomvideo/add.ts @@ -5,25 +5,32 @@ import prisma from "../../../../lib/prisma"; const client_id = process.env.ZOOM_CLIENT_ID; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === 'GET') { - // Check that user is authenticated - const session = await getSession({req: req}); + if (req.method === "GET") { + // Check that user is authenticated + const session = await getSession({ req: req }); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } - - // Get user - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, - }, - select: { - id: true - } - }); - - const redirectUri = encodeURI(process.env.BASE_URL + '/api/integrations/zoomvideo/callback'); - const authUrl = 'https://zoom.us/oauth/authorize?response_type=code&client_id=' + client_id + '&redirect_uri=' + redirectUri; - - res.status(200).json({url: authUrl}); + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; } + + // Get user + await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + }, + }); + + const redirectUri = encodeURI(process.env.BASE_URL + "/api/integrations/zoomvideo/callback"); + const authUrl = + "https://zoom.us/oauth/authorize?response_type=code&client_id=" + + client_id + + "&redirect_uri=" + + redirectUri; + + res.status(200).json({ url: authUrl }); + } } diff --git a/pages/api/integrations/zoomvideo/callback.ts b/pages/api/integrations/zoomvideo/callback.ts index 3b2449c53b..fb379cbbcf 100644 --- a/pages/api/integrations/zoomvideo/callback.ts +++ b/pages/api/integrations/zoomvideo/callback.ts @@ -1,39 +1,40 @@ -import type {NextApiRequest, NextApiResponse} from 'next'; -import {getSession} from "next-auth/client"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { getSession } from "next-auth/client"; import prisma from "../../../../lib/prisma"; const client_id = process.env.ZOOM_CLIENT_ID; const client_secret = process.env.ZOOM_CLIENT_SECRET; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { code } = req.query; + const { code } = req.query; - // Check that user is authenticated - const session = await getSession({req: req}); + // Check that user is authenticated + const session = await getSession({ req: req }); - if (!session) { res.status(401).json({message: 'You must be logged in to do this'}); return; } + if (!session) { + res.status(401).json({ message: "You must be logged in to do this" }); + return; + } - const redirectUri = encodeURI(process.env.BASE_URL + '/api/integrations/zoomvideo/callback'); - const authHeader = 'Basic ' + Buffer.from(client_id + ':' + client_secret).toString('base64'); + const redirectUri = encodeURI(process.env.BASE_URL + "/api/integrations/zoomvideo/callback"); + const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64"); + const result = await fetch( + "https://zoom.us/oauth/token?grant_type=authorization_code&code=" + code + "&redirect_uri=" + redirectUri, + { + method: "POST", + headers: { + Authorization: authHeader, + }, + } + ); + const json = await result.json(); - return new Promise( async (resolve, reject) => { - const result = await fetch('https://zoom.us/oauth/token?grant_type=authorization_code&code=' + code + '&redirect_uri=' + redirectUri, { - method: 'POST', - headers: { - Authorization: authHeader - } - }) - .then(res => res.json()); - - await prisma.credential.create({ - data: { - type: 'zoom_video', - key: result, - userId: session.user.id - } - }); - - res.redirect('/integrations'); - resolve(); - }); -} \ No newline at end of file + await prisma.credential.create({ + data: { + type: "zoom_video", + key: json, + userId: session.user.id, + }, + }); + res.redirect("/integrations"); +} diff --git a/pages/api/teams/[team]/index.ts b/pages/api/teams/[team]/index.ts index 3fde85bc80..624304233a 100644 --- a/pages/api/teams/[team]/index.ts +++ b/pages/api/teams/[team]/index.ts @@ -1,26 +1,25 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import prisma from '../../../../lib/prisma'; -import {getSession} from "next-auth/client"; +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "../../../../lib/prisma"; +import { getSession } from "next-auth/client"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - - const session = await getSession({req: req}); + const session = await getSession({ req: req }); if (!session) { - return res.status(401).json({message: "Not authenticated"}); + return res.status(401).json({ message: "Not authenticated" }); } // DELETE /api/teams/{team} if (req.method === "DELETE") { - const deleteMembership = await prisma.membership.delete({ + await prisma.membership.delete({ where: { - userId_teamId: { userId: session.user.id, teamId: parseInt(req.query.team) } - } + userId_teamId: { userId: session.user.id, teamId: parseInt(req.query.team) }, + }, }); - const deleteTeam = await prisma.team.delete({ + await prisma.team.delete({ where: { id: parseInt(req.query.team), }, }); return res.status(204).send(null); } -} \ No newline at end of file +} diff --git a/pages/api/teams/[team]/invite.ts b/pages/api/teams/[team]/invite.ts index b595e87eda..3b7cdf64b4 100644 --- a/pages/api/teams/[team]/invite.ts +++ b/pages/api/teams/[team]/invite.ts @@ -1,38 +1,33 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import prisma from '../../../../lib/prisma'; +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "../../../../lib/prisma"; import createInvitationEmail from "../../../../lib/emails/invitation"; -import {getSession} from "next-auth/client"; -import {randomBytes} from "crypto"; -import {create} from "domain"; +import { getSession } from "next-auth/client"; +import { randomBytes } from "crypto"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== "POST") { return res.status(400).json({ message: "Bad request" }); } - const session = await getSession({req: req}); + const session = await getSession({ req: req }); if (!session) { - return res.status(401).json({message: "Not authenticated"}); + return res.status(401).json({ message: "Not authenticated" }); } const team = await prisma.team.findFirst({ where: { - id: parseInt(req.query.team) - } + id: parseInt(req.query.team), + }, }); if (!team) { - return res.status(404).json({message: "Invalid team"}); + return res.status(404).json({ message: "Invalid team" }); } const invitee = await prisma.user.findFirst({ where: { - OR: [ - { username: req.body.usernameOrEmail }, - { email: req.body.usernameOrEmail } - ] - } + OR: [{ username: req.body.usernameOrEmail }, { email: req.body.usernameOrEmail }], + }, }); if (!invitee) { @@ -41,33 +36,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!isEmail(req.body.usernameOrEmail)) { return res.status(400).json({ - message: `Invite failed because there is no corresponding user for ${req.body.usernameOrEmail}` + message: `Invite failed because there is no corresponding user for ${req.body.usernameOrEmail}`, }); } // valid email given, create User - const createUser = await prisma.user.create( - { + await prisma.user + .create({ data: { email: req.body.usernameOrEmail, - } + }, }) - .then( (invitee) => prisma.membership.create( - { + .then((invitee) => + prisma.membership.create({ data: { - teamId: parseInt(req.query.team), + teamId: parseInt(req.query.team as string), userId: invitee.id, role: req.body.role, }, - })); + }) + ); const token: string = randomBytes(32).toString("hex"); - const createVerificationRequest = await prisma.verificationRequest.create({ + await prisma.verificationRequest.create({ data: { identifier: req.body.usernameOrEmail, token, - expires: new Date((new Date()).setHours(168)) // +1 week - } + expires: new Date(new Date().setHours(168)), // +1 week + }, }); createInvitationEmail({ @@ -82,30 +78,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // create provisional membership try { - const createMembership = await prisma.membership.create({ + await prisma.membership.create({ data: { - teamId: parseInt(req.query.team), + teamId: parseInt(req.query.team as string), userId: invitee.id, role: req.body.role, }, }); - } - catch (err) { - if (err.code === "P2002") { // unique constraint violation + } catch (err) { + if (err.code === "P2002") { + // unique constraint violation return res.status(409).json({ - message: 'This user is a member of this team / has a pending invitation.', + message: "This user is a member of this team / has a pending invitation.", }); } else { throw err; // rethrow } - }; + } // inform user of membership by email if (req.body.sendEmailInvitation) { createInvitationEmail({ toEmail: invitee.email, from: session.user.name, - teamName: team.name + teamName: team.name, }); } diff --git a/pages/api/teams/[team]/membership.ts b/pages/api/teams/[team]/membership.ts index 19d93abe91..8cd437e42f 100644 --- a/pages/api/teams/[team]/membership.ts +++ b/pages/api/teams/[team]/membership.ts @@ -1,26 +1,25 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import prisma from '../../../../lib/prisma'; -import {getSession} from "next-auth/client"; +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "../../../../lib/prisma"; +import { getSession } from "next-auth/client"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - - const session = await getSession({req}); + const session = await getSession({ req }); if (!session) { - res.status(401).json({message: "Not authenticated"}); + res.status(401).json({ message: "Not authenticated" }); return; } - const isTeamOwner = !!await prisma.membership.findFirst({ + const isTeamOwner = !!(await prisma.membership.findFirst({ where: { userId: session.user.id, - teamId: parseInt(req.query.team), - role: 'OWNER' - } - }); + teamId: parseInt(req.query.team as string), + role: "OWNER", + }, + })); - if ( ! isTeamOwner) { - res.status(403).json({message: "You are not authorized to manage this team"}); + if (!isTeamOwner) { + res.status(403).json({ message: "You are not authorized to manage this team" }); return; } @@ -28,24 +27,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (req.method === "GET") { const memberships = await prisma.membership.findMany({ where: { - teamId: parseInt(req.query.team), - } + teamId: parseInt(req.query.team as string), + }, }); let members = await prisma.user.findMany({ where: { id: { - in: memberships.map( (membership) => membership.userId ), - } - } + in: memberships.map((membership) => membership.userId), + }, + }, }); - members = members.map( (member) => { - const membership = memberships.find( (membership) => member.id === membership.userId ); + members = members.map((member) => { + const membership = memberships.find((membership) => member.id === membership.userId); return { ...member, - role: membership.accepted ? membership.role : 'INVITEE', - } + role: membership.accepted ? membership.role : "INVITEE", + }; }); return res.status(200).json({ members: members }); @@ -53,10 +52,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Cancel a membership (invite) if (req.method === "DELETE") { - const memberships = await prisma.membership.delete({ + await prisma.membership.delete({ where: { userId_teamId: { userId: req.body.userId, teamId: parseInt(req.query.team) }, - } + }, }); return res.status(204).send(null); } diff --git a/pages/api/user/membership.ts b/pages/api/user/membership.ts index d05ae5b39d..de48fcf504 100644 --- a/pages/api/user/membership.ts +++ b/pages/api/user/membership.ts @@ -1,34 +1,33 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import prisma from '../../../lib/prisma'; +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "../../../lib/prisma"; import { getSession } from "next-auth/client"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - - const session = await getSession({req: req}); + const session = await getSession({ req: req }); if (!session) { - return res.status(401).json({message: "Not authenticated"}); + return res.status(401).json({ message: "Not authenticated" }); } if (req.method === "GET") { const memberships = await prisma.membership.findMany({ where: { userId: session.user.id, - } + }, }); const teams = await prisma.team.findMany({ where: { id: { - in: memberships.map(membership => membership.teamId), - } - } + in: memberships.map((membership) => membership.teamId), + }, + }, }); return res.status(200).json({ membership: memberships.map((membership) => ({ - role: membership.accepted ? membership.role : 'INVITEE', - ...teams.find(team => team.id === membership.teamId) - })) + role: membership.accepted ? membership.role : "INVITEE", + ...teams.find((team) => team.id === membership.teamId), + })), }); } @@ -38,25 +37,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Leave team or decline membership invite of current user if (req.method === "DELETE") { - const memberships = await prisma.membership.delete({ + await prisma.membership.delete({ where: { - userId_teamId: { userId: session.user.id, teamId: req.body.teamId } - } + userId_teamId: { userId: session.user.id, teamId: req.body.teamId }, + }, }); return res.status(204).send(null); } // Accept team invitation if (req.method === "PATCH") { - const memberships = await prisma.membership.update({ + await prisma.membership.update({ where: { - userId_teamId: { userId: session.user.id, teamId: req.body.teamId } + userId_teamId: { userId: session.user.id, teamId: req.body.teamId }, }, data: { - accepted: true - } + accepted: true, + }, }); return res.status(204).send(null); } -} \ No newline at end of file +} diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index b7b1f7f54c..b4af7f6fb9 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -238,7 +238,6 @@ export default function EventTypePage({ }, }); - router.push("/event-types"); showToast("Event Type updated", "success"); setSuccessModalOpen(true); @@ -325,7 +324,7 @@ export default function EventTypePage({ name="address" id="address" required - className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm" + className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm" defaultValue={locations.find((location) => location.type === LocationType.InPerson)?.address} /> @@ -381,26 +380,26 @@ export default function EventTypePage({ name="title" id="title" required - className="pl-0 text-xl font-bold text-gray-900 cursor-pointer border-none focus:ring-0 bg-transparent focus:outline-none" + className="pl-0 text-xl font-bold text-gray-900 bg-transparent border-none cursor-pointer focus:ring-0 focus:outline-none" placeholder="Quick Chat" defaultValue={eventType.title} /> } subtitle={eventType.description}>
-
-
+
+
-
-
-