integration page follow ups (#912)
### Internals - Replace `lodash.*` packages with plain `lodash` & replace `lodash.*` imports with `lodash/` - should have no impact on bundle size and opens up for us to use all of lodash - Update `viewer.me` to cherry-pick what we actually need on that query to avoid leaking extra context info - Update `getIntegrations` to never include `.key`-property to avoid leaking ### Visual - Update calendars so `primary` is displayed last - Update connected calendars so they are in ascending order in which you connected them
This commit is contained in:
1 parent
9e2f8de313
commit
ec6b897191
18 files changed
+146
-147
No files matched your search
@@ -11,7 +11,6 @@ NEXT_PUBLIC_LICENSE_CONSENT=''
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5432/calendso?schema=public"
|
||||
|
||||
GOOGLE_API_CREDENTIALS='secret'
|
||||
GOOGLE_REDIRECT_URL='https://localhost:3000/integrations/googlecalendar/callback'
|
||||
|
||||
BASE_URL='http://localhost:3000'
|
||||
NEXT_PUBLIC_APP_URL='http://localhost:3000'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import merge from "lodash.merge";
|
||||
import merge from "lodash/merge";
|
||||
import { NextSeo, NextSeoProps } from "next-seo";
|
||||
import React from "react";
|
||||
|
||||
|
||||
Vendored
-1
@@ -3,7 +3,6 @@ declare namespace NodeJS {
|
||||
readonly CALENDSO_ENCRYPTION_KEY: string | undefined;
|
||||
readonly DATABASE_URL: string | undefined;
|
||||
readonly GOOGLE_API_CREDENTIALS: string | undefined;
|
||||
readonly GOOGLE_REDIRECT_URL: string | undefined;
|
||||
readonly BASE_URL: string | undefined;
|
||||
readonly NEXT_PUBLIC_BASE_URL: string | undefined;
|
||||
readonly NEXT_PUBLIC_APP_URL: string | undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Credential } from "@prisma/client";
|
||||
import async from "async";
|
||||
import merge from "lodash.merge";
|
||||
import merge from "lodash/merge";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
|
||||
|
||||
@@ -2,21 +2,17 @@ import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogProps,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
} from "@components/Dialog";
|
||||
import { Form, TextField } from "@components/form/fields";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
export const ADD_APPLE_INTEGRATION_FORM_TITLE = "addAppleIntegration";
|
||||
|
||||
export function AddAppleIntegrationModal(props: DialogProps) {
|
||||
@@ -104,49 +100,3 @@ export function AddAppleIntegrationModal(props: DialogProps) {
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
const AddAppleIntegration = React.forwardRef<HTMLFormElement, Props>((props, ref) => {
|
||||
const onSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
props.onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<form id={ADD_APPLE_INTEGRATION_FORM_TITLE} ref={ref} onSubmit={onSubmit}>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="email@icloud.com"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="•••••••••••••"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
AddAppleIntegration.displayName = "AddAppleIntegrationForm";
|
||||
export default AddAppleIntegration;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import { validJson } from "@lib/jsonUtils";
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true, key: true },
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
|
||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||
@@ -33,6 +34,14 @@ export const ALL_INTEGRATIONS = [
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: !!process.env.DAILY_API_KEY,
|
||||
type: "daily_video",
|
||||
title: "Daily.co Video",
|
||||
imageSrc: "integrations/daily.svg",
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "caldav_calendar",
|
||||
@@ -62,23 +71,33 @@ export const ALL_INTEGRATIONS = [
|
||||
variant: "payment",
|
||||
},
|
||||
] as const;
|
||||
function getIntegrations(credentials: CredentialData[]) {
|
||||
const integrations = ALL_INTEGRATIONS.map((integration) => ({
|
||||
...integration,
|
||||
/**
|
||||
* @deprecated use `credentials.
|
||||
*/
|
||||
credential: credentials.find((credential) => credential.type === integration.type) || null,
|
||||
credentials: credentials.filter((credential) => credential.type === integration.type) || null,
|
||||
}));
|
||||
|
||||
function getIntegrations(userCredentials: CredentialData[]) {
|
||||
const integrations = ALL_INTEGRATIONS.map((integration) => {
|
||||
const credentials = userCredentials
|
||||
.filter((credential) => credential.type === integration.type)
|
||||
.map((credential) => _.pick(credential, ["id", "type"])); // ensure we don't leak `key` to frontend
|
||||
|
||||
const credential: typeof credentials[number] | null = credentials[0] || null;
|
||||
return {
|
||||
...integration,
|
||||
/**
|
||||
* @deprecated use `credentials`
|
||||
*/
|
||||
credential,
|
||||
credentials,
|
||||
};
|
||||
});
|
||||
|
||||
return integrations;
|
||||
}
|
||||
|
||||
export type IntegraionMeta = ReturnType<typeof getIntegrations>;
|
||||
export type IntegrationMeta = ReturnType<typeof getIntegrations>;
|
||||
|
||||
export function hasIntegration(integrations: ReturnType<typeof getIntegrations>, type: string): boolean {
|
||||
return !!integrations.find((i) => i.type === type && !!i.installed && !!i.credential);
|
||||
export function hasIntegration(integrations: IntegrationMeta, type: string): boolean {
|
||||
return !!integrations.find(
|
||||
(i) => i.type === type && !!i.installed && (type === "daily_video" || i.credentials.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
export default getIntegrations;
|
||||
+4
-7
@@ -27,8 +27,8 @@
|
||||
"yarn": ">=1.19.0 < 2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@daily-co/daily-js": "^0.16.0",
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@hookform/resolvers": "^2.8.1",
|
||||
"@jitsu/sdk-js": "^2.2.4",
|
||||
@@ -61,9 +61,7 @@
|
||||
"ical.js": "^1.4.0",
|
||||
"ics": "^2.31.0",
|
||||
"jimp": "^0.16.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"micro": "^9.3.4",
|
||||
"next": "^11.1.1",
|
||||
"next-auth": "^3.28.0",
|
||||
@@ -82,10 +80,10 @@
|
||||
"react-multi-email": "^0.5.3",
|
||||
"react-phone-number-input": "^3.1.25",
|
||||
"react-query": "^3.23.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^4.3.1",
|
||||
"react-timezone-select": "^1.0.7",
|
||||
"react-use-intercom": "1.4.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"short-uuid": "^4.2.0",
|
||||
"stripe": "^8.168.0",
|
||||
"superjson": "1.7.5",
|
||||
@@ -100,8 +98,7 @@
|
||||
"@types/async": "^3.2.7",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash": "^4.14.175",
|
||||
"@types/micro": "^7.3.6",
|
||||
"@types/node": "^16.6.1",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
|
||||
@@ -20,8 +20,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
// Get token from Google Calendar API
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.GOOGLE_REDIRECT_URL || redirect_uris[0];
|
||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback";
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
|
||||
@@ -25,8 +25,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return;
|
||||
}
|
||||
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.GOOGLE_REDIRECT_URL || redirect_uris[0];
|
||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
||||
const redirect_uri = process.env.BASE_URL + "/api/integrations/googlecalendar/callback";
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
const token = await oAuth2Client.getToken(code);
|
||||
const key = token.res?.data;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ResetPasswordRequest } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import debounce from "lodash.debounce";
|
||||
import debounce from "lodash/debounce";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/client";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import debounce from "lodash.debounce";
|
||||
import debounce from "lodash/debounce";
|
||||
import { getCsrfToken } from "next-auth/client";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
@@ -1247,7 +1247,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const locationOptions: OptionTypeBase[] = [
|
||||
{ value: LocationType.InPerson, label: "Link or In-person meeting" },
|
||||
{ value: LocationType.Phone, label: "Phone call" },
|
||||
{ value: LocationType.Zoom, label: "Zoom Video", disabled: true },
|
||||
];
|
||||
|
||||
if (hasIntegration(integrations, "zoom_video")) {
|
||||
@@ -1257,8 +1256,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
if (hasIntegration(integrations, "google_calendar")) {
|
||||
locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
|
||||
}
|
||||
const hasDailyIntegration = process.env.DAILY_API_KEY;
|
||||
if (hasDailyIntegration) {
|
||||
if (hasIntegration(integrations, "daily_video")) {
|
||||
locationOptions.push({ value: LocationType.Daily, label: "Daily.co Video" });
|
||||
}
|
||||
const currency =
|
||||
|
||||
@@ -11,12 +11,12 @@ import classnames from "classnames";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import debounce from "lodash.debounce";
|
||||
import debounce from "lodash/debounce";
|
||||
import omit from "lodash/omit";
|
||||
import { NextPageContext } from "next";
|
||||
import { useSession } from "next-auth/client";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { Integration } from "pages/integrations/_new";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import TimezoneSelect from "react-timezone-select";
|
||||
|
||||
@@ -691,7 +691,7 @@ export async function getServerSideProps(context: NextPageContext) {
|
||||
},
|
||||
});
|
||||
|
||||
integrations = getIntegrations(credentials);
|
||||
integrations = getIntegrations(credentials).map((item) => omit(item, "key"));
|
||||
|
||||
eventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Maybe } from "@trpc/server";
|
||||
import Image from "next/image";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
@@ -8,7 +7,7 @@ import classNames from "@lib/classNames";
|
||||
import { AddAppleIntegrationModal } from "@lib/integrations/Apple/components/AddAppleIntegration";
|
||||
import { AddCalDavIntegrationModal } from "@lib/integrations/CalDav/components/AddCalDavIntegration";
|
||||
import showToast from "@lib/notification";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import { Dialog } from "@components/Dialog";
|
||||
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
|
||||
@@ -19,8 +18,6 @@ import Badge from "@components/ui/Badge";
|
||||
import Button, { ButtonBaseProps } from "@components/ui/Button";
|
||||
import Switch from "@components/ui/Switch";
|
||||
|
||||
type IntegrationCalendar = inferQueryOutput<"viewer.integrations">["calendar"]["items"][number];
|
||||
|
||||
function pluralize(opts: { num: number; plural: string; singular: string }) {
|
||||
if (opts.num === 0) {
|
||||
return opts.singular;
|
||||
@@ -47,10 +44,7 @@ function SubHeadingTitleWithConnections(props: { title: ReactNode; numConnection
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectIntegration(props: {
|
||||
type: IntegrationCalendar["type"];
|
||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||
}) {
|
||||
function ConnectIntegration(props: { type: string; render: (renderProps: ButtonBaseProps) => JSX.Element }) {
|
||||
const { type } = props;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const mutation = useMutation(async () => {
|
||||
@@ -154,14 +148,15 @@ function DisconnectIntegration(props: {
|
||||
|
||||
function ConnectOrDisconnectIntegrationButton(props: {
|
||||
//
|
||||
credential: Maybe<{ id: number }>;
|
||||
type: IntegrationCalendar["type"];
|
||||
credentialIds: number[];
|
||||
type: string;
|
||||
installed: boolean;
|
||||
}) {
|
||||
if (props.credential) {
|
||||
const [credentialId] = props.credentialIds;
|
||||
if (credentialId) {
|
||||
return (
|
||||
<DisconnectIntegration
|
||||
id={props.credential.id}
|
||||
id={credentialId}
|
||||
render={(btnProps) => (
|
||||
<Button {...btnProps} color="warn">
|
||||
Disconnect
|
||||
@@ -177,6 +172,14 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/** We don't need to "Connect", just show that it's installed */
|
||||
if (props.type === "daily_video") {
|
||||
return (
|
||||
<div className="px-3 py-2 truncate">
|
||||
<h3 className="text-sm font-medium text-gray-700">Installed</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ConnectIntegration type={props.type} render={(btnProps) => <Button {...btnProps}>Connect</Button>} />
|
||||
);
|
||||
@@ -193,7 +196,7 @@ function IntegrationListItem(props: {
|
||||
<ListItem expanded={!!props.children} className={classNames("flex-col")}>
|
||||
<div className={classNames("flex flex-1 space-x-2 w-full p-4 items-center")}>
|
||||
<Image width={40} height={40} src={`/${props.imageSrc}`} alt={props.title} />
|
||||
<div className="pl-2 flex-grow truncate">
|
||||
<div className="flex-grow pl-2 truncate">
|
||||
<ListItemTitle component="h3">{props.title}</ListItemTitle>
|
||||
<ListItemText component="p">{props.description}</ListItemText>
|
||||
</div>
|
||||
@@ -205,7 +208,7 @@ function IntegrationListItem(props: {
|
||||
}
|
||||
|
||||
export function CalendarSwitch(props: {
|
||||
type: IntegrationCalendar["type"];
|
||||
type: string;
|
||||
externalId: string;
|
||||
title: string;
|
||||
defaultSelected: boolean;
|
||||
@@ -353,7 +356,7 @@ export default function IntegrationsPage() {
|
||||
)}
|
||||
/>
|
||||
}>
|
||||
<ul className="space-y-2 p-4">
|
||||
<ul className="p-4 space-y-2">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1.25em" height="1.25em" viewBox="0, 0, 400,400">
|
||||
<path id="path0"
|
||||
d="M100.400 142.062 C 99.630 142.280,98.394 143.076,97.654 143.830 C 96.914 144.583,95.997 145.200,95.616 145.200 C 94.776 145.200,93.802 146.248,93.389 147.598 C 93.221 148.147,92.560 149.054,91.919 149.613 C 90.024 151.267,90.020 151.390,90.010 199.645 C 89.999 248.545,90.014 248.945,91.940 250.744 C 92.571 251.334,93.229 252.262,93.401 252.808 C 93.751 253.916,95.054 255.200,95.829 255.200 C 96.107 255.200,96.710 255.808,97.169 256.550 C 98.373 258.498,94.832 258.400,164.273 258.400 C 231.741 258.400,231.099 258.418,231.949 256.552 C 232.208 255.983,233.149 255.250,234.197 254.801 C 235.357 254.304,236.005 253.774,236.014 253.314 C 236.021 252.921,236.375 251.880,236.800 251.000 C 237.225 250.120,237.579 249.119,237.586 248.776 C 237.594 248.434,237.864 247.804,238.187 247.376 C 238.696 246.704,238.776 240.392,238.787 200.426 C 238.801 149.852,238.967 154.051,236.799 149.949 C 236.610 149.591,236.332 148.647,236.183 147.850 C 235.956 146.640,235.591 146.227,233.964 145.342 C 232.893 144.759,231.907 143.938,231.774 143.518 C 231.641 143.098,231.052 142.539,230.466 142.277 C 229.079 141.657,102.567 141.447,100.400 142.062 "
|
||||
stroke="none" fill="#f9f9f9" fillRule="evenodd"></path>
|
||||
<path id="path1"
|
||||
d="M304.600 153.562 C 304.160 153.717,302.589 154.419,301.109 155.122 C 299.629 155.825,298.171 156.400,297.869 156.400 C 297.567 156.400,296.528 156.977,295.560 157.682 C 294.592 158.387,292.872 159.272,291.739 159.649 C 290.605 160.025,288.743 160.976,287.602 161.761 C 286.460 162.547,284.778 163.386,283.863 163.628 C 282.948 163.869,281.300 164.672,280.200 165.413 C 279.100 166.154,277.660 166.885,277.000 167.037 C 275.491 167.385,272.800 168.718,272.800 169.117 C 272.800 169.485,270.749 170.506,268.629 171.194 C 266.207 171.979,263.730 174.650,263.412 176.820 C 262.921 180.167,263.353 224.092,263.889 225.295 C 264.635 226.970,266.755 228.668,269.300 229.629 C 270.565 230.107,271.600 230.622,271.600 230.775 C 271.600 231.219,274.452 232.687,276.241 233.162 C 277.144 233.403,278.381 234.061,278.991 234.626 C 279.600 235.191,281.382 236.125,282.950 236.701 C 284.517 237.278,286.430 238.236,287.200 238.831 C 287.970 239.426,289.320 240.126,290.200 240.387 C 292.160 240.967,294.400 242.079,294.400 242.472 C 294.400 242.837,297.518 244.231,299.125 244.584 C 299.790 244.730,300.737 245.198,301.228 245.625 C 301.720 246.051,302.620 246.400,303.228 246.400 C 303.837 246.400,304.605 246.504,304.936 246.631 C 305.267 246.758,305.902 246.498,306.348 246.052 C 306.793 245.607,307.721 244.951,308.410 244.595 C 310.905 243.305,310.800 245.287,310.800 199.575 C 310.800 155.897,310.789 155.600,309.169 155.600 C 309.026 155.600,308.231 155.060,307.400 154.400 C 306.569 153.740,305.780 153.218,305.645 153.240 C 305.510 153.262,305.040 153.407,304.600 153.562 "
|
||||
stroke="none" fill="#1be7b8" fillRule="evenodd"></path>
|
||||
<path id="path2"
|
||||
d="M104.148 137.776 C 103.459 138.076,102.774 138.519,102.624 138.760 C 102.475 139.002,101.832 139.200,101.196 139.200 C 98.679 139.200,95.594 140.337,94.191 141.782 C 93.434 142.562,92.630 143.200,92.406 143.200 C 92.181 143.200,91.703 143.875,91.344 144.700 C 90.984 145.525,90.140 146.560,89.467 147.000 C 87.556 148.251,87.579 147.532,87.693 201.219 L 87.800 252.069 88.800 252.944 C 89.350 253.425,90.311 254.498,90.935 255.328 C 91.559 256.159,92.682 257.235,93.430 257.719 C 94.178 258.204,94.792 258.829,94.795 259.110 C 94.801 259.708,96.289 260.360,98.770 260.851 C 99.743 261.044,100.887 261.516,101.311 261.901 C 102.535 263.008,223.251 262.983,224.942 261.875 C 225.616 261.433,227.174 261.056,228.925 260.910 C 232.411 260.620,234.281 259.898,234.866 258.616 C 235.107 258.087,235.812 257.444,236.432 257.187 C 237.635 256.688,238.800 255.226,238.800 254.214 C 238.800 253.876,239.039 253.600,239.330 253.600 C 239.622 253.600,240.297 253.135,240.830 252.568 L 241.800 251.536 241.800 200.335 L 241.800 149.134 240.400 147.884 C 239.630 147.197,238.690 145.944,238.312 145.101 C 237.852 144.075,237.232 143.430,236.441 143.154 C 235.696 142.895,235.110 142.318,234.859 141.598 C 234.411 140.311,233.008 139.763,229.068 139.333 C 227.786 139.194,226.522 138.865,226.260 138.603 C 224.854 137.196,225.002 137.200,164.726 137.216 C 115.566 137.229,105.185 137.325,104.148 137.776 M230.299 140.581 C 231.013 140.751,232.363 141.600,233.299 142.466 C 234.235 143.333,235.488 144.338,236.085 144.699 C 236.684 145.061,237.282 145.862,237.419 146.487 C 237.556 147.110,238.076 148.110,238.574 148.710 C 240.721 151.291,240.592 148.280,240.713 198.600 C 240.829 246.814,240.750 249.650,239.248 251.152 C 238.800 251.600,238.071 252.676,237.629 253.543 C 237.187 254.410,236.187 255.514,235.407 255.995 C 234.628 256.477,233.798 257.231,233.563 257.670 C 232.125 260.355,229.256 260.458,160.200 260.300 C 96.040 260.154,98.009 260.223,96.185 258.055 C 95.663 257.435,94.598 256.495,93.818 255.964 C 93.037 255.434,92.310 254.730,92.202 254.400 C 92.094 254.070,91.396 253.117,90.652 252.283 C 88.728 250.126,88.809 252.440,88.804 199.526 C 88.800 148.835,88.746 150.246,90.767 148.075 C 91.445 147.347,92.000 146.583,92.000 146.379 C 92.000 145.965,94.367 143.600,94.781 143.600 C 94.926 143.600,95.721 142.979,96.550 142.220 C 97.645 141.217,98.567 140.772,99.928 140.589 C 100.958 140.450,101.980 140.273,102.200 140.195 C 103.020 139.904,229.052 140.284,230.299 140.581 M302.261 151.784 C 301.415 152.085,300.477 152.683,300.177 153.111 C 299.589 153.951,298.498 154.440,295.467 155.223 C 294.179 155.556,293.257 156.096,292.706 156.841 C 292.120 157.635,291.307 158.082,289.909 158.382 C 287.523 158.894,286.569 159.361,285.000 160.786 C 284.254 161.463,282.944 162.058,281.536 162.358 C 279.852 162.717,278.929 163.194,277.936 164.216 C 277.201 164.973,276.327 165.593,275.994 165.596 C 274.726 165.605,271.323 167.114,270.329 168.107 C 269.759 168.678,268.506 169.354,267.546 169.609 C 263.906 170.578,262.647 172.127,261.546 176.994 C 260.707 180.702,260.406 219.312,261.200 221.401 C 261.420 221.979,261.860 223.699,262.178 225.222 C 262.801 228.210,263.915 229.763,265.769 230.228 C 266.340 230.371,266.906 230.649,267.027 230.844 C 267.148 231.040,267.598 231.200,268.028 231.200 C 268.457 231.200,269.121 231.575,269.504 232.034 C 270.324 233.017,272.827 234.231,274.800 234.604 C 275.626 234.760,276.610 235.349,277.200 236.040 C 277.950 236.919,278.976 237.422,281.300 238.052 C 283.242 238.578,284.400 239.096,284.400 239.438 C 284.400 240.158,287.095 241.510,289.201 241.847 C 290.693 242.085,292.400 243.256,292.400 244.041 C 292.400 244.329,297.174 246.000,297.997 246.000 C 298.233 246.000,299.057 246.630,299.827 247.400 C 301.156 248.729,301.366 248.800,303.981 248.800 L 306.736 248.800 309.338 246.578 C 312.714 243.696,312.469 247.711,312.322 197.737 L 312.200 156.074 310.962 154.537 C 308.533 151.521,305.601 150.593,302.261 151.784 M307.400 154.400 C 308.231 155.060,309.026 155.600,309.169 155.600 C 310.789 155.600,310.800 155.897,310.800 199.575 C 310.800 245.287,310.905 243.305,308.410 244.595 C 307.721 244.951,306.793 245.607,306.348 246.052 C 305.902 246.498,305.267 246.758,304.936 246.631 C 304.605 246.504,303.837 246.400,303.228 246.400 C 302.620 246.400,301.720 246.051,301.228 245.625 C 300.737 245.198,299.790 244.730,299.125 244.584 C 297.518 244.231,294.400 242.837,294.400 242.472 C 294.400 242.079,292.160 240.967,290.200 240.387 C 289.320 240.126,287.970 239.426,287.200 238.831 C 286.430 238.236,284.517 237.278,282.950 236.701 C 281.382 236.125,279.600 235.191,278.991 234.626 C 278.381 234.061,277.144 233.403,276.241 233.162 C 274.452 232.687,271.600 231.219,271.600 230.775 C 271.600 230.622,270.565 230.107,269.300 229.629 C 266.755 228.668,264.635 226.970,263.889 225.295 C 263.353 224.092,262.921 180.167,263.412 176.820 C 263.730 174.650,266.207 171.979,268.629 171.194 C 270.749 170.506,272.800 169.485,272.800 169.117 C 272.800 Line truncated
|
||||
stroke="none" fill="#4c545c" fillRule="evenodd"></path>
|
||||
<path id="path3"
|
||||
d="M102.200 140.195 C 101.980 140.273,100.958 140.450,99.928 140.589 C 98.567 140.772,97.645 141.217,96.550 142.220 C 95.721 142.979,94.926 143.600,94.781 143.600 C 94.367 143.600,92.000 145.965,92.000 146.379 C 92.000 146.583,91.445 147.347,90.767 148.075 C 88.746 150.246,88.800 148.835,88.804 199.526 C 88.809 252.440,88.728 250.126,90.652 252.283 C 91.396 253.117,92.094 254.070,92.202 254.400 C 92.310 254.730,93.037 255.434,93.818 255.964 C 94.598 256.495,95.663 257.435,96.185 258.055 C 98.009 260.223,96.040 260.154,160.200 260.300 C 229.256 260.458,232.125 260.355,233.563 257.670 C 233.798 257.231,234.628 256.477,235.407 255.995 C 236.187 255.514,237.187 254.410,237.629 253.543 C 238.071 252.676,238.800 251.600,239.248 251.152 C 240.750 249.650,240.829 246.814,240.713 198.600 C 240.592 148.280,240.721 151.291,238.574 148.710 C 238.076 148.110,237.556 147.110,237.419 146.487 C 237.282 145.862,236.684 145.061,236.085 144.699 C 235.488 144.338,234.235 143.333,233.299 142.466 C 232.363 141.600,231.013 140.751,230.299 140.581 C 229.052 140.284,103.020 139.904,102.200 140.195 M230.466 142.277 C 231.052 142.539,231.641 143.098,231.774 143.518 C 231.907 143.938,232.893 144.759,233.964 145.342 C 235.591 146.227,235.956 146.640,236.183 147.850 C 236.332 148.647,236.610 149.591,236.799 149.949 C 238.967 154.051,238.801 149.852,238.787 200.426 C 238.776 240.392,238.696 246.704,238.187 247.376 C 237.864 247.804,237.594 248.434,237.586 248.776 C 237.579 249.119,237.225 250.120,236.800 251.000 C 236.375 251.880,236.021 252.921,236.014 253.314 C 236.005 253.774,235.357 254.304,234.197 254.801 C 233.149 255.250,232.208 255.983,231.949 256.552 C 231.099 258.418,231.741 258.400,164.273 258.400 C 94.832 258.400,98.373 258.498,97.169 256.550 C 96.710 255.808,96.107 255.200,95.829 255.200 C 95.054 255.200,93.751 253.916,93.401 252.808 C 93.229 252.262,92.571 251.334,91.940 250.744 C 90.014 248.945,89.999 248.545,90.010 199.645 C 90.020 151.390,90.024 151.267,91.919 149.613 C 92.560 149.054,93.221 148.147,93.389 147.598 C 93.802 146.248,94.776 145.200,95.616 145.200 C 95.997 145.200,96.914 144.583,97.654 143.830 C 98.394 143.076,99.630 142.280,100.400 142.062 C 102.567 141.447,229.079 141.657,230.466 142.277 "
|
||||
stroke="none" fill="#949c9c" fillRule="evenodd"></path>
|
||||
<path id="path4"
|
||||
d="M35.200 0.984 C 35.200 1.947,35.121 1.971,31.700 2.084 L 28.200 2.200 28.077 3.900 L 27.954 5.600 25.403 5.600 C 21.914 5.600,20.903 6.043,20.590 7.712 C 20.367 8.902,20.142 9.103,18.669 9.430 C 17.102 9.777,16.988 9.898,16.800 11.400 C 16.605 12.956,16.554 13.003,14.922 13.122 C 13.260 13.243,13.243 13.260,13.122 14.922 C 13.003 16.554,12.956 16.605,11.400 16.800 C 9.898 16.988,9.777 17.102,9.430 18.669 C 9.103 20.142,8.902 20.367,7.712 20.590 C 6.043 20.903,5.600 21.914,5.600 25.403 L 5.600 27.954 3.900 28.077 L 2.200 28.200 2.084 31.700 C 1.971 35.121,1.947 35.200,0.984 35.200 L 0.000 35.200 0.000 200.000 L 0.000 364.800 0.984 364.800 C 1.947 364.800,1.971 364.879,2.084 368.300 L 2.200 371.800 3.900 372.177 L 5.600 372.554 5.600 374.851 C 5.600 378.083,6.072 379.102,7.712 379.410 C 8.902 379.633,9.103 379.858,9.430 381.331 C 9.777 382.898,9.898 383.012,11.400 383.200 C 12.953 383.394,13.004 383.449,13.121 385.059 C 13.247 386.786,13.757 387.181,15.876 387.195 C 16.598 387.199,16.773 387.463,16.876 388.700 C 16.992 390.104,17.107 390.224,18.669 390.570 C 20.142 390.897,20.367 391.098,20.590 392.288 C 20.903 393.957,21.914 394.400,25.403 394.400 L 27.954 394.400 28.077 396.100 L 28.200 397.800 31.700 397.916 C 35.121 398.029,35.200 398.053,35.200 399.016 L 35.200 400.000 200.000 400.000 L 364.800 400.000 364.800 399.016 C 364.800 398.053,364.879 398.029,368.300 397.916 L 371.800 397.800 372.177 396.100 L 372.554 394.400 375.103 394.400 C 378.233 394.400,379.094 393.974,379.414 392.265 C 379.633 391.101,379.865 390.896,381.331 390.570 C 382.893 390.224,383.008 390.104,383.124 388.700 C 383.241 387.288,383.327 387.200,384.596 387.200 C 386.308 387.200,387.200 386.308,387.200 384.596 C 387.200 383.327,387.288 383.241,388.700 383.124 C 390.104 383.008,390.224 382.893,390.570 381.331 C 390.896 379.865,391.101 379.633,392.265 379.414 C 393.974 379.094,394.400 378.233,394.400 375.103 L 394.400 372.554 396.100 372.177 L 397.800 371.800 397.916 368.300 C 398.029 364.879,398.053 364.800,399.016 364.800 L 400.000 364.800 400.000 200.000 L 400.000 35.200 399.016 35.200 C 398.053 35.200,398.029 35.121,397.916 31.700 L 397.800 28.200 396.100 28.077 L 394.400 27.954 394.400 25.403 C 394.400 21.914,393.957 20.903,392.288 20.590 C 391.098 20.367,390.897 20.142,390.570 18.669 C 390.224 17.107,390.104 16.992,388.700 16.876 C 387.463 16.773,387.199 16.598,387.195 15.876 C 387.181 13.757,386.786 13.247,385.059 13.121 C 383.452 13.004,383.396 12.953,383.275 11.480 C 383.121 9.617,382.265 9.200,378.597 9.200 L 376.046 9.200 375.923 7.500 C 375.802 5.821,375.779 5.798,374.173 5.681 C 372.616 5.566,372.529 5.488,372.173 3.881 L 371.800 2.200 368.300 2.084 C 364.879 1.971,364.800 1.947,364.800 0.984 L 364.800 0.000 200.000 0.000 L 35.200 0.000 35.200 0.984 M224.918 137.663 C 225.394 137.918,225.998 138.341,226.260 138.603 C 226.522 138.865,227.786 139.194,229.068 139.333 C 233.008 139.763,234.411 140.311,234.859 141.598 C 235.110 142.318,235.696 142.895,236.441 143.154 C 237.232 143.430,237.852 144.075,238.312 145.101 C 238.690 145.944,239.630 147.197,240.400 147.884 L 241.800 149.134 241.800 200.335 L 241.800 251.536 240.830 252.568 C 240.297 253.135,239.622 253.600,239.330 253.600 C 239.039 253.600,238.800 253.876,238.800 254.214 C 238.800 255.226,237.635 256.688,236.432 257.187 C 235.812 257.444,235.107 258.087,234.866 258.616 C 234.281 259.898,232.411 260.620,228.925 260.910 C 227.174 261.056,225.616 261.433,224.942 261.875 C 223.251 262.983,102.535 263.008,101.311 261.901 C 100.887 261.516,99.743 261.044,98.770 260.851 C 96.289 260.360,94.801 259.708,94.795 259.110 C 94.792 258.829,94.178 258.204,93.430 257.719 C 92.682 257.235,91.559 256.159,90.935 255.328 C 90.311 254.498,89.350 253.425,88.800 252.944 L 87.800 252.069 87.693 201.219 C 87.579 147.532,87.556 148.251,89.467 147.000 C 90.140 146.560,90.984 145.525,91.344 144.700 C 91.703 143.875,92.181 143.200,92.406 143.200 C 92.630 143.200,93.434 142.562,94.191 141.782 C 95.594 140.337,98.679 139.200,101.196 139.200 C 101.832 139.200,102.475 139.002,102.624 138.760 C 103.575 137.222,103.193 137.232,164.726 137.216 C 208.933 137.204,224.273 137.318,224.918 137.663 M308.162 152.107 C 309.021 152.598,310.281 153.692,310.962 154.537 L 312.200 156.074 312.322 197.737 C 312.469 247.711,312.714 243.696,309.338 246.578 L 306.736 248.800 303.981 248.800 C 301.366 248.800,301.156 248.729,299.827 247.400 C 299.057 246.630,298.233 246.000,297.997 246.000 C 297.174 246.000,292.400 244.329,292.400 244.041 C 292.400 243.256,290.693 242.085,289.201 241.847 C 287.095 241.510,284.400 240.158,284.400 239.438 C 284.400 239.096,283.242 238.578,281.300 238.052 C 278.976 237.422,277.950 236.919,277.200 236.040 C 276.610 235.349,275.626 234.760,274.800 234.604 C 272.827 234.231,270.324 233.017,269.504 232.034 C 269.121 231.575,268.457 231.200,268.028 231.200 C 267.598 231.200,267.148 231.040,267.027 230.844 C 266.906 230.649,266.340 230.371,265.769 230.228 C 263.915 229.763,262.801 228.21Line truncated
|
||||
stroke="none" fill="#141c24" fillRule="evenodd"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -38,6 +38,9 @@ async function getUserFromSession({ session, req }: { session: Maybe<Session>; r
|
||||
type: true,
|
||||
key: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
selectedCalendars: {
|
||||
select: {
|
||||
|
||||
+52
-18
@@ -1,12 +1,13 @@
|
||||
import { BookingStatus, Prisma } from "@prisma/client";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import _ from "lodash";
|
||||
import { getErrorFromUnknown } from "pages/_error";
|
||||
import { z } from "zod";
|
||||
|
||||
import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername";
|
||||
|
||||
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
|
||||
import getIntegrations, { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
|
||||
import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
import { getCalendarAdapterOrNull } from "../../lib/calendarClient";
|
||||
@@ -20,7 +21,34 @@ const checkUsername =
|
||||
export const viewerRouter = createProtectedRouter()
|
||||
.query("me", {
|
||||
resolve({ ctx }) {
|
||||
return ctx.user;
|
||||
const {
|
||||
// pick only the part we want to expose in the API
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
startTime,
|
||||
endTime,
|
||||
bufferTime,
|
||||
locale,
|
||||
avatar,
|
||||
createdDate,
|
||||
completedOnboarding,
|
||||
} = ctx.user;
|
||||
const me = {
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
startTime,
|
||||
endTime,
|
||||
bufferTime,
|
||||
locale,
|
||||
avatar,
|
||||
createdDate,
|
||||
completedOnboarding,
|
||||
};
|
||||
return me;
|
||||
},
|
||||
})
|
||||
.query("bookings", {
|
||||
@@ -98,11 +126,17 @@ export const viewerRouter = createProtectedRouter()
|
||||
async resolve({ ctx }) {
|
||||
const { user } = ctx;
|
||||
const { credentials } = user;
|
||||
const integrations = getIntegrations(credentials);
|
||||
|
||||
function countActive(items: { credentials: unknown[] }[]) {
|
||||
return items.reduce((acc, item) => acc + item.credentials.length, 0);
|
||||
function countActive(items: { credentialIds: unknown[] }[]) {
|
||||
return items.reduce((acc, item) => acc + item.credentialIds.length, 0);
|
||||
}
|
||||
const integrations = ALL_INTEGRATIONS.map((integration) => ({
|
||||
...integration,
|
||||
credentialIds: credentials
|
||||
.filter((credential) => credential.type === integration.type)
|
||||
.map((credential) => credential.id),
|
||||
}));
|
||||
// `flatMap()` these work like `.filter()` but infers the types correctly
|
||||
const conferencing = integrations.flatMap((item) => (item.variant === "conferencing" ? [item] : []));
|
||||
const payment = integrations.flatMap((item) => (item.variant === "payment" ? [item] : []));
|
||||
const calendar = integrations.flatMap((item) => (item.variant === "calendar" ? [item] : []));
|
||||
@@ -126,25 +160,24 @@ export const viewerRouter = createProtectedRouter()
|
||||
const connectedCalendars = await Promise.all(
|
||||
calendarCredentials.map(async (item) => {
|
||||
const { adapter, integration, credential } = item;
|
||||
|
||||
const credentialId = credential.id;
|
||||
try {
|
||||
const _calendars = await adapter.listCalendars();
|
||||
const calendars = _calendars.map((cal) => ({
|
||||
...cal,
|
||||
isSelected: !!user.selectedCalendars.find((selected) => selected.externalId === cal.externalId),
|
||||
}));
|
||||
const cals = await adapter.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => ({
|
||||
...cal,
|
||||
isSelected: user.selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
}))
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars[0];
|
||||
if (!primary) {
|
||||
return {
|
||||
integration,
|
||||
credentialId: credential.id,
|
||||
error: {
|
||||
message: "No primary calendar found",
|
||||
},
|
||||
};
|
||||
throw new Error("No primary calendar found");
|
||||
}
|
||||
return {
|
||||
integration,
|
||||
credentialId: credential.id,
|
||||
credentialId,
|
||||
primary,
|
||||
calendars,
|
||||
};
|
||||
@@ -152,6 +185,7 @@ export const viewerRouter = createProtectedRouter()
|
||||
const error = getErrorFromUnknown(_error);
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
error: {
|
||||
message: error.message,
|
||||
},
|
||||
|
||||
@@ -1790,26 +1790,14 @@
|
||||
version "7.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
||||
|
||||
"@types/lodash.debounce@^4.0.6":
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash.merge@^4.6.6":
|
||||
version "4.6.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.6.tgz#b84b403c1d31bc42d51772d1cd5557fa008cd3d6"
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*", "@types/lodash@^4.14.165":
|
||||
version "4.14.175"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
|
||||
|
||||
"@types/lodash@4.14.168":
|
||||
version "4.14.168"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
|
||||
|
||||
"@types/lodash@^4.14.165", "@types/lodash@^4.14.175":
|
||||
version "4.14.175"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
|
||||
|
||||
"@types/micro@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/micro/-/micro-7.3.6.tgz#7d68eb5a780ac4761e3b80687b4ee7328ebc3f2e"
|
||||
@@ -5712,10 +5700,6 @@ lodash.clonedeep@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
|
||||
lodash.flattendeep@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
|
||||
@@ -5757,10 +5741,6 @@ lodash.sortby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
|
||||
lodash.throttle@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||
|
||||
lodash.topath@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009"
|
||||
|
||||
Reference in new issue
Block a user