Compare commits

...
6 Commits
Author SHA1 Message Date
Alex van Andel 5bd0428d18 v3.8.0
release.yml / release (push) Canceled after 0s
2024-02-15 15:54:47 +00:00
c7089b9db6 feat: store event-type filters to localStorage (#13407)
* Updated useTypedQuery to store values to localstorage

* removed the redundant function params

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-02-15 10:03:42 -05:00
Morgan 81ad00a816 fix: safely delete userPassword (#13705)
* fix: safely delete userPassword

* fixup! fix: safely delete userPassword

* fixup! Merge branch 'main' into fix-safely-delete-userpassword
2024-02-15 13:19:31 +00:00
Udit TakkarandMorgan b5730da56d fix: password reset when provider is changed (#13706)
* fix: password reset when provider is changed

* chore: remove log

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-02-15 13:19:11 +00:00
Zeeshan BhatiandJoe Au-Yeung 9455d14ad5 feat: Add Google Profile Photo when connecting Google Calendar (#13627)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-02-15 11:51:24 +00:00
LAVIN RAJ MOHANandUdit Takkar a056160404 fix(fix/null-safety):#13673 (#13674)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-02-15 07:17:19 +00:00
8 changed files with 94 additions and 11 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.7.19",
"version": "3.8.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+3 -2
View File
@@ -42,8 +42,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
data: {
password: {
update: {
hash: hashedPassword,
upsert: {
create: { hash: hashedPassword },
update: { hash: hashedPassword },
},
},
emailVerified: new Date(),
@@ -47,7 +47,7 @@ export default function Apps({ apps }: PageProps) {
<div className="mb-16">
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
{apps
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
?.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
.map((app) => {
return <AppCard key={app.slug} app={app} />;
})}
@@ -11,6 +11,7 @@ import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
export const scopes = [
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/userinfo.profile",
];
let client_id = "";
@@ -1,9 +1,11 @@
import type { Auth } from "googleapis";
import { google } from "googleapis";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL_FOR_OAUTH, WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
@@ -91,6 +93,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
throw new HttpError({ message: "Internal Error", statusCode: 500 });
}
// Update the user's profile photo with google profile photo if it's null
// Since we don't want to block the user from using the app if this fails, we don't await this
updateProfilePhoto(oAuth2Client, req.session.user.id);
const credential = await prisma.credential.create({
data: {
type: "google_calendar",
@@ -156,6 +162,23 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
);
}
async function updateProfilePhoto(oAuth2Client: Auth.OAuth2Client, userId: number) {
try {
const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client });
const userDetails = await oauth2.userinfo.get();
if (userDetails.data?.picture) {
await prisma.user.update({
where: { id: userId, avatarUrl: null, avatar: null },
data: {
avatarUrl: userDetails.data.picture,
},
});
}
} catch (error) {
logger.error("Error updating avatarUrl from google calendar connect", error);
}
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});
@@ -1,4 +1,5 @@
import type { Membership, Team, UserPermissionRole } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import type { AuthOptions, Session } from "next-auth";
import type { JWT } from "next-auth/jwt";
import { encode } from "next-auth/jwt";
@@ -806,18 +807,32 @@ export const AUTH_OPTIONS: AuthOptions = {
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
) {
await prisma.user.update({
const updatedUser = await prisma.user.update({
where: { email: existingUserWithEmail.email },
// also update email to the IdP email
data: {
password: {
delete: true,
},
email: user.email,
identityProvider: idP,
identityProviderId: account.providerAccountId,
},
});
// safely delete password from UserPassword table if it exists
try {
await prisma.userPassword.delete({
where: { userId: updatedUser.id },
});
} catch (err) {
if (
err instanceof PrismaClientKnownRequestError &&
(err.code === "P2025" || err.code === "P2016")
) {
log.warn("UserPassword not found for user", safeStringify(existingUserWithEmail));
} else {
log.warn("Could not delete UserPassword for user", safeStringify(existingUserWithEmail));
}
}
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail.email);
} else {
@@ -17,9 +17,10 @@ import { filterQuerySchema } from "../lib/getTeamsFiltersFromQuery";
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
export type IEventTypeFilter = IEventTypesFilters[0];
export const TEAMS_FILTER_KEY = "TEAMS_FILTER_KEY";
function useFilterQuery() {
// passthrough allows additional params to not be removed
return useTypedQuery(filterQuerySchema.passthrough());
return useTypedQuery(filterQuerySchema.passthrough(), TEAMS_FILTER_KEY);
}
export const TeamsFilter = ({
+44 -2
View File
@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation";
import { useCallback, useMemo, useEffect } from "react";
import { z } from "zod";
import { localStorage } from "../webstorage";
import { useRouterQuery } from "./useRouterQuery";
type OptionalKeys<T> = {
@@ -32,7 +33,7 @@ export const queryStringArray = z
.preprocess((a) => z.string().parse(a).split(","), z.string().array())
.or(z.string().array());
export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
export function useTypedQuery<T extends z.AnyZodObject>(schema: T, localStorageKey: string | null = null) {
type Output = z.infer<typeof schema>;
type FullOutput = Required<Output>;
type OutputKeys = Required<keyof FullOutput>;
@@ -43,7 +44,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
const unparsedQuery = useRouterQuery();
const pathname = usePathname();
const parsedQuerySchema = schema.safeParse(unparsedQuery);
const useLocalStorage = !!localStorageKey;
let parsedQuery: Output = useMemo(() => {
return {} as Output;
}, []);
@@ -73,6 +74,39 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
// eslint-disable-next-line react-hooks/exhaustive-deps
[parsedQuery, router]
);
//If the value for {localStorageKey} exist in localStorage set it to queryParams
useEffect(() => {
if (!useLocalStorage) return;
const storedValueString = localStorage.getItem(localStorageKey);
if (!storedValueString || storedValueString == "{}") return;
const storedValue: Output = JSON.parse(storedValueString);
if (storedValue) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(storedValue)) {
searchParams.set(String(key), String(value));
}
router.replace(`${pathname}?${searchParams.toString()}`);
}
}, []);
function setLocalStorage<J extends OutputKeys>(key: J, value: Output[J]) {
if (!useLocalStorage) return;
const storedValue: Output = JSON.parse(localStorage.getItem(localStorageKey) || "{}");
const newValue = { ...storedValue, [key]: value };
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
}
function removeByKeyFromLocalStorage(key: OutputOptionalKeys) {
if (!useLocalStorage) return;
const storedValue: Output = JSON.parse(localStorage.getItem(localStorageKey) || "{}");
const newValue = storedValue;
delete newValue[key];
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
}
//Remove {localStorageKey} from the localStorage
function removeAllValuesFromLocalStorage() {
if (!useLocalStorage) return;
localStorage.removeItem(localStorageKey);
}
// Delete a key from the query
function removeByKey(key: OutputOptionalKeys) {
@@ -88,9 +122,13 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
if (existingValue.includes(value)) return; // prevent adding the same value to the array
// @ts-expect-error this is too much for TS it seems
setQuery(key, [...existingValue, value]);
// @ts-expect-error this is too much for TS it seems
setLocalStorage(key, [...existingValue, value]);
} else {
// @ts-expect-error this is too much for TS it seems
setQuery(key, [value]);
// @ts-expect-error this is too much for TS it seems
setLocalStorage(key, [value]);
}
}
@@ -101,9 +139,12 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
// @ts-expect-error this is too much for TS it seems
const newValue = existingValue.filter((item) => item !== value);
setQuery(key, newValue);
setLocalStorage(key, newValue);
} else {
// @ts-expect-error this is too much for TS it seems
removeByKey(key);
//@ts-expect-error this is too much for TS it seems
removeByKeyFromLocalStorage(key);
}
}
@@ -111,6 +152,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
function removeAllQueryParams() {
if (pathname !== null) {
router.replace(pathname);
removeAllValuesFromLocalStorage();
}
}