Session Timeout (#6439)
* Implementation * Unifying UI to password screen * Tweaks for session in password section * Update apps/web/pages/settings/security/password.tsx Co-authored-by: Omar López <zomars@me.com> * Update packages/features/kbar/Kbar.tsx Co-authored-by: Omar López <zomars@me.com> * Update packages/features/settings/layouts/SettingsLayout.tsx Co-authored-by: Omar López <zomars@me.com> * Relying on extra db query * Upgrades typescript * Update yarn lock * Typings * Hotfix: ping,riverside,whereby and around not showing up in list (#6712) * Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713) * Adds deployment settings to DB (#6706) * WIP * Adds DeploymentTheme * Add missing migrations * Adds client extensions for deployment * Cleanup * Using same version as other deps * Reverting prisma changes and fixing things * Uneeded tx-expect-error * Fixing default value * Update apps/web/public/static/locales/en/common.json Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> --------- Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
parent
9ab5a3149c
commit
36b6c351ec
|
@ -74,6 +74,7 @@
|
|||
"handlebars": "^4.7.7",
|
||||
"ical.js": "^1.4.0",
|
||||
"ics": "^2.37.0",
|
||||
"jose": "^4.11.1",
|
||||
"kbar": "^0.1.0-beta.36",
|
||||
"libphonenumber-js": "^1.10.12",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { IdentityProvider, UserPermissionRole } from "@prisma/client";
|
||||
import { BinaryLike, hkdfSync, KeyObject } from "crypto";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import * as jose from "jose";
|
||||
import NextAuth, { Session } from "next-auth";
|
||||
import { Provider } from "next-auth/providers";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
|
@ -9,6 +11,7 @@ import GoogleProvider from "next-auth/providers/google";
|
|||
import nodemailer, { TransportOptions } from "nodemailer";
|
||||
import { authenticator } from "otplib";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||
|
@ -22,7 +25,7 @@ import rateLimit from "@calcom/lib/rateLimit";
|
|||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||
|
||||
|
@ -60,6 +63,7 @@ const providers: Provider[] = [
|
|||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
identityProvider: true,
|
||||
password: true,
|
||||
twoFactorEnabled: true,
|
||||
|
@ -235,6 +239,9 @@ if (true) {
|
|||
);
|
||||
}
|
||||
const calcomAdapter = CalComAdapter(prisma);
|
||||
const getDerivedEncryptionKey = async (secret: BinaryLike | KeyObject) => {
|
||||
return await hkdfSync("sha256", secret, "", "NextAuth.js Generated Encryption Key", 32);
|
||||
};
|
||||
export default NextAuth({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
@ -242,6 +249,37 @@ export default NextAuth({
|
|||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
encode: async ({ secret, token }) => {
|
||||
if (!token || token.sub === undefined) throw new Error("Not valid token");
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: Number(token.sub) },
|
||||
select: { metadata: true },
|
||||
});
|
||||
const encryptionSecret = await getDerivedEncryptionKey(secret);
|
||||
const metadata = userMetadata.parse(user?.metadata);
|
||||
return await new jose.EncryptJWT({
|
||||
sub: token?.sub,
|
||||
name: token?.name,
|
||||
email: token?.email,
|
||||
})
|
||||
.setProtectedHeader({
|
||||
alg: "dir",
|
||||
enc: "A256GCM",
|
||||
})
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(metadata?.sessionTimeout ? `${metadata.sessionTimeout}m` : "30d")
|
||||
.setJti(uuidv4())
|
||||
.encrypt(new Uint8Array(encryptionSecret));
|
||||
},
|
||||
decode: async ({ secret, token }) => {
|
||||
const encryptionSecret = await getDerivedEncryptionKey(secret);
|
||||
const { payload } = await jose.jwtDecrypt(token || "", new Uint8Array(encryptionSecret), {
|
||||
clockTolerance: 15,
|
||||
});
|
||||
return payload;
|
||||
},
|
||||
},
|
||||
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
|
|
|
@ -16,6 +16,8 @@ import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
|||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
|
@ -31,8 +33,6 @@ import {
|
|||
EmptyScreen,
|
||||
showToast,
|
||||
Switch,
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
Tooltip,
|
||||
HorizontalTabs,
|
||||
} from "@calcom/ui";
|
||||
|
|
|
@ -1,51 +1,96 @@
|
|||
import { IdentityProvider } from "@prisma/client";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { identityProviderNameMap } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Form, Meta, PasswordField, showToast } from "@calcom/ui";
|
||||
import { Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
type ChangePasswordFormValues = {
|
||||
type ChangePasswordSessionFormValues = {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
sessionTimeout?: number;
|
||||
};
|
||||
|
||||
const PasswordView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const metadata = userMetadata.parse(user?.metadata);
|
||||
|
||||
const mutation = trpc.viewer.auth.changePassword.useMutation({
|
||||
const sessionMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("session_timeout_changed"), "success");
|
||||
formMethods.reset(formMethods.getValues());
|
||||
},
|
||||
onSettled: () => {
|
||||
utils.viewer.me.invalidate();
|
||||
},
|
||||
onMutate: async ({ metadata }) => {
|
||||
await utils.viewer.me.cancel();
|
||||
const previousValue = utils.viewer.me.getData();
|
||||
const previousMetadata = userMetadata.parse(previousValue?.metadata);
|
||||
|
||||
if (previousValue && metadata?.sessionTimeout) {
|
||||
utils.viewer.me.setData(undefined, {
|
||||
...previousValue,
|
||||
metadata: { ...previousMetadata, sessionTimeout: metadata?.sessionTimeout },
|
||||
});
|
||||
}
|
||||
return { previousValue };
|
||||
},
|
||||
onError: (error, _, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.me.setData(undefined, context.previousValue);
|
||||
}
|
||||
showToast(`${t("session_timeout_change_error")}, ${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
const passwordMutation = trpc.viewer.auth.changePassword.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("password_has_been_changed"), "success");
|
||||
formMethods.resetField("oldPassword");
|
||||
formMethods.resetField("newPassword");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`${t("error_updating_password")}, ${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const formMethods = useForm<ChangePasswordFormValues>({
|
||||
const formMethods = useForm<ChangePasswordSessionFormValues>({
|
||||
defaultValues: {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
sessionTimeout: metadata?.sessionTimeout,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
} = formMethods;
|
||||
const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
|
||||
|
||||
const handleSubmit = (values: ChangePasswordFormValues) => {
|
||||
const { oldPassword, newPassword } = values;
|
||||
mutation.mutate({ oldPassword, newPassword });
|
||||
const handleSubmit = (values: ChangePasswordSessionFormValues) => {
|
||||
const { oldPassword, newPassword, sessionTimeout } = values;
|
||||
if (oldPassword && newPassword) {
|
||||
passwordMutation.mutate({ oldPassword, newPassword });
|
||||
}
|
||||
if (metadata?.sessionTimeout !== sessionTimeout) {
|
||||
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout } });
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutOptions = [5, 10, 15].map((mins) => ({
|
||||
label: t("multiple_duration_mins", { count: mins }),
|
||||
value: mins,
|
||||
}));
|
||||
|
||||
const isDisabled = formMethods.formState.isSubmitting || !formMethods.formState.isDirty;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("password")} description={t("password_description")} />
|
||||
|
@ -68,24 +113,59 @@ const PasswordView = () => {
|
|||
<Form form={formMethods} handleSubmit={handleSubmit}>
|
||||
<div className="max-w-[38rem] sm:flex sm:space-x-4">
|
||||
<div className="flex-grow">
|
||||
<PasswordField {...register("oldPassword")} label={t("old_password")} />
|
||||
<PasswordField {...formMethods.register("oldPassword")} label={t("old_password")} />
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<PasswordField {...register("newPassword")} label={t("new_password")} />
|
||||
<PasswordField {...formMethods.register("newPassword")} label={t("new_password")} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="mt-4 max-w-[38rem] text-sm text-gray-600">
|
||||
<Trans i18nKey="invalid_password_hint">
|
||||
Password must be at least at least 7 characters, mix of uppercase & lowercase letters, and
|
||||
contain at least 1 number
|
||||
contain at least 1 number.
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="mt-8 border-t border-gray-200 py-8">
|
||||
<SettingsToggle
|
||||
title={t("session_timeout")}
|
||||
description={t("session_timeout_description")}
|
||||
checked={sessionTimeoutWatch !== undefined}
|
||||
data-testid="session-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("sessionTimeout", undefined, { shouldDirty: true });
|
||||
} else {
|
||||
formMethods.setValue("sessionTimeout", 10, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{sessionTimeoutWatch && (
|
||||
<div className="mt-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("session_timeout_after")}</p>
|
||||
<Select
|
||||
options={timeoutOptions}
|
||||
defaultValue={
|
||||
metadata?.sessionTimeout
|
||||
? timeoutOptions.find((tmo) => tmo.value === metadata.sessionTimeout)
|
||||
: timeoutOptions[1]
|
||||
}
|
||||
isSearchable={false}
|
||||
className="block h-[36px] !w-auto min-w-0 flex-none rounded-md text-sm"
|
||||
onChange={(event) => {
|
||||
formMethods.setValue("sessionTimeout", event?.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* TODO: Why is this Form not submitting? Hacky fix but works */}
|
||||
<Button
|
||||
color="primary"
|
||||
className="mt-8"
|
||||
type="submit"
|
||||
disabled={isSubmitting || mutation.isLoading}>
|
||||
disabled={isDisabled || passwordMutation.isLoading || sessionMutation.isLoading}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Form>
|
||||
|
|
|
@ -414,6 +414,8 @@
|
|||
"password_updated_successfully": "Password updated successfully",
|
||||
"password_has_been_changed": "Your password has been successfully changed.",
|
||||
"error_changing_password": "Error changing password",
|
||||
"session_timeout_changed": "Your session configuration has been updated successfully.",
|
||||
"session_timeout_change_error": "Error updating session configuration",
|
||||
"something_went_wrong": "Something went wrong.",
|
||||
"something_doesnt_look_right": "Something doesn't look right?",
|
||||
"please_try_again": "Please try again.",
|
||||
|
@ -939,6 +941,11 @@
|
|||
"current_location": "Current Location",
|
||||
"user_phone": "Your phone number",
|
||||
"new_location": "New Location",
|
||||
"session": "Session",
|
||||
"session_description": "Control your account session",
|
||||
"session_timeout_after": "Timeout session after",
|
||||
"session_timeout": "Session timeout",
|
||||
"session_timeout_description": "Invalidate your session after a certain amount of time.",
|
||||
"no_location": "No location defined",
|
||||
"set_location": "Set Location",
|
||||
"update_location": "Update Location",
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Dispatch, SetStateAction } from "react";
|
|||
|
||||
import { getQueryBuilderConfig } from "../lib/getQueryBuilderConfig";
|
||||
import isRouterLinkedField from "../lib/isRouterLinkedField";
|
||||
import { SerializableForm, Response } from "../types/types";
|
||||
import { Response, SerializableForm } from "../types/types";
|
||||
|
||||
type Props = {
|
||||
form: SerializableForm<App_RoutingForms_Form>;
|
||||
|
|
|
@ -7,7 +7,7 @@ import DOMPurify from "dompurify";
|
|||
import { useSession } from "next-auth/react";
|
||||
import React, { AriaRole, ComponentType, Fragment } from "react";
|
||||
|
||||
import { APP_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { FiAlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
|
|
@ -208,6 +208,7 @@ export const userMetadata = z
|
|||
stripeCustomerId: z.string().optional(),
|
||||
vitalSettings: vitalSettingsUpdateSchema.optional(),
|
||||
isPremium: z.boolean().optional(),
|
||||
sessionTimeout: z.number().optional(), // Minutes
|
||||
})
|
||||
.nullable();
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
|||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||
import { ErrorCode, verifyPassword } from "@calcom/lib/auth";
|
||||
import { IS_SELF_HOSTED, IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
|
@ -586,12 +585,14 @@ const loggedInViewerRouter = router({
|
|||
locale: z.string().optional(),
|
||||
timeFormat: z.number().optional(),
|
||||
disableImpersonation: z.boolean().optional(),
|
||||
metadata: userMetadata.optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { user, prisma } = ctx;
|
||||
const data: Prisma.UserUpdateInput = {
|
||||
...input,
|
||||
metadata: input.metadata as Prisma.InputJsonValue,
|
||||
};
|
||||
let isPremiumUsername = false;
|
||||
if (input.username) {
|
||||
|
@ -1129,7 +1130,7 @@ const loggedInViewerRouter = router({
|
|||
roomName: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
.query(async ({ input }) => {
|
||||
const { roomName } = input;
|
||||
try {
|
||||
const res = await getRecordingsOfCalVideoByRoomName(roomName);
|
||||
|
|
Loading…
Reference in New Issue
Block a user