diff --git a/components/Avatar.tsx b/components/Avatar.tsx index 02cf38a9..b1f58d63 100644 --- a/components/Avatar.tsx +++ b/components/Avatar.tsx @@ -1,37 +1,22 @@ -import { useState } from "react"; -import md5 from "../lib/md5"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import { defaultAvatarSrc } from "@lib/profile"; -export default function Avatar({ - user, - className = "", - fallback, - imageSrc = "", -}: { - user: any; +export type AvatarProps = { className?: string; - fallback?: JSX.Element; imageSrc?: string; -}) { - const [gravatarAvailable, setGravatarAvailable] = useState(true); + displayName: string; + gravatarFallbackMd5?: string; +}; - if (imageSrc) { - return 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} - /> - ); - } - - return fallback || null; +export default function Avatar({ imageSrc, displayName, gravatarFallbackMd5, className = "" }: AvatarProps) { + return ( + + + + {gravatarFallbackMd5 && ( + {displayName} + )} + + + ); } diff --git a/components/Shell.tsx b/components/Shell.tsx index a8d6b3f1..312219c3 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; -import React, { Fragment, useEffect } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import { useRouter } from "next/router"; -import { useSession } from "next-auth/client"; +import { signOut, useSession } from "next-auth/client"; import { Menu, Transition } from "@headlessui/react"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../lib/telemetry"; import { SelectorIcon } from "@heroicons/react/outline"; @@ -18,6 +18,8 @@ import { import Logo from "./Logo"; import classNames from "@lib/classNames"; import { Toaster } from "react-hot-toast"; +import Avatar from "@components/Avatar"; +import { User } from "@prisma/client"; export default function Shell(props) { const router = useRouter(); @@ -112,7 +114,7 @@ export default function Shell(props) {
- +
@@ -191,45 +193,52 @@ export default function Shell(props) { ) : null; } -function UserDropdown({ session, small, bottom }: { session: any; small?: boolean; bottom?: boolean }) { +function UserDropdown({ small, bottom }: { small?: boolean; bottom?: boolean }) { + const [user, setUser] = useState(null); + + useEffect(() => { + fetch("/api/me") + .then((res) => res.json()) + .then((responseBody) => { + setUser(responseBody.user); + }); + }, []); + return ( {({ open }) => ( <>
- - - - - {!small && ( - - {session.user.name} - - /{session.user.username} + {user && ( + + + + + {!small && ( + + {user?.name} + + /{user?.username} + - + )} + + {!small && ( + - {!small && ( - - + + )}
- + View public page
@@ -311,22 +320,21 @@ function UserDropdown({ session, small, bottom }: { session: any; small?: boolea
{({ active }) => ( - - signOut({ callbackUrl: "/auth/logout" })} + className={classNames( + active ? "bg-gray-100 text-gray-900" : "text-gray-700", + "flex px-4 py-2 text-sm font-medium" + )}> + - - + "text-neutral-400 group-hover:text-neutral-500", + "mr-2 flex-shrink-0 h-5 w-5" + )} + aria-hidden="true" + /> + Sign out + )}
diff --git a/lib/md5.js b/lib/md5.js deleted file mode 100644 index 67f06c64..00000000 --- a/lib/md5.js +++ /dev/null @@ -1,173 +0,0 @@ -function md5cycle(x, k) { - let a = x[0], - b = x[1], - c = x[2], - d = x[3]; - - a = ff(a, b, c, d, k[0], 7, -680876936); - d = ff(d, a, b, c, k[1], 12, -389564586); - c = ff(c, d, a, b, k[2], 17, 606105819); - b = ff(b, c, d, a, k[3], 22, -1044525330); - a = ff(a, b, c, d, k[4], 7, -176418897); - d = ff(d, a, b, c, k[5], 12, 1200080426); - c = ff(c, d, a, b, k[6], 17, -1473231341); - b = ff(b, c, d, a, k[7], 22, -45705983); - a = ff(a, b, c, d, k[8], 7, 1770035416); - d = ff(d, a, b, c, k[9], 12, -1958414417); - c = ff(c, d, a, b, k[10], 17, -42063); - b = ff(b, c, d, a, k[11], 22, -1990404162); - a = ff(a, b, c, d, k[12], 7, 1804603682); - d = ff(d, a, b, c, k[13], 12, -40341101); - c = ff(c, d, a, b, k[14], 17, -1502002290); - b = ff(b, c, d, a, k[15], 22, 1236535329); - - a = gg(a, b, c, d, k[1], 5, -165796510); - d = gg(d, a, b, c, k[6], 9, -1069501632); - c = gg(c, d, a, b, k[11], 14, 643717713); - b = gg(b, c, d, a, k[0], 20, -373897302); - a = gg(a, b, c, d, k[5], 5, -701558691); - d = gg(d, a, b, c, k[10], 9, 38016083); - c = gg(c, d, a, b, k[15], 14, -660478335); - b = gg(b, c, d, a, k[4], 20, -405537848); - a = gg(a, b, c, d, k[9], 5, 568446438); - d = gg(d, a, b, c, k[14], 9, -1019803690); - c = gg(c, d, a, b, k[3], 14, -187363961); - b = gg(b, c, d, a, k[8], 20, 1163531501); - a = gg(a, b, c, d, k[13], 5, -1444681467); - d = gg(d, a, b, c, k[2], 9, -51403784); - c = gg(c, d, a, b, k[7], 14, 1735328473); - b = gg(b, c, d, a, k[12], 20, -1926607734); - - a = hh(a, b, c, d, k[5], 4, -378558); - d = hh(d, a, b, c, k[8], 11, -2022574463); - c = hh(c, d, a, b, k[11], 16, 1839030562); - b = hh(b, c, d, a, k[14], 23, -35309556); - a = hh(a, b, c, d, k[1], 4, -1530992060); - d = hh(d, a, b, c, k[4], 11, 1272893353); - c = hh(c, d, a, b, k[7], 16, -155497632); - b = hh(b, c, d, a, k[10], 23, -1094730640); - a = hh(a, b, c, d, k[13], 4, 681279174); - d = hh(d, a, b, c, k[0], 11, -358537222); - c = hh(c, d, a, b, k[3], 16, -722521979); - b = hh(b, c, d, a, k[6], 23, 76029189); - a = hh(a, b, c, d, k[9], 4, -640364487); - d = hh(d, a, b, c, k[12], 11, -421815835); - c = hh(c, d, a, b, k[15], 16, 530742520); - b = hh(b, c, d, a, k[2], 23, -995338651); - - a = ii(a, b, c, d, k[0], 6, -198630844); - d = ii(d, a, b, c, k[7], 10, 1126891415); - c = ii(c, d, a, b, k[14], 15, -1416354905); - b = ii(b, c, d, a, k[5], 21, -57434055); - a = ii(a, b, c, d, k[12], 6, 1700485571); - d = ii(d, a, b, c, k[3], 10, -1894986606); - c = ii(c, d, a, b, k[10], 15, -1051523); - b = ii(b, c, d, a, k[1], 21, -2054922799); - a = ii(a, b, c, d, k[8], 6, 1873313359); - d = ii(d, a, b, c, k[15], 10, -30611744); - c = ii(c, d, a, b, k[6], 15, -1560198380); - b = ii(b, c, d, a, k[13], 21, 1309151649); - a = ii(a, b, c, d, k[4], 6, -145523070); - d = ii(d, a, b, c, k[11], 10, -1120210379); - c = ii(c, d, a, b, k[2], 15, 718787259); - b = ii(b, c, d, a, k[9], 21, -343485551); - - x[0] = add32(a, x[0]); - x[1] = add32(b, x[1]); - x[2] = add32(c, x[2]); - x[3] = add32(d, x[3]); -} - -function cmn(q, a, b, x, s, t) { - a = add32(add32(a, q), add32(x, t)); - return add32((a << s) | (a >>> (32 - s)), b); -} - -function ff(a, b, c, d, x, s, t) { - return cmn((b & c) | (~b & d), a, b, x, s, t); -} - -function gg(a, b, c, d, x, s, t) { - return cmn((b & d) | (c & ~d), a, b, x, s, t); -} - -function hh(a, b, c, d, x, s, t) { - return cmn(b ^ c ^ d, a, b, x, s, t); -} - -function ii(a, b, c, d, x, s, t) { - return cmn(c ^ (b | ~d), a, b, x, s, t); -} - -function md51(s) { - 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); - 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); - for (i = 0; i < 16; i++) tail[i] = 0; - } - tail[14] = n * 8; - md5cycle(state, tail); - return state; -} - -/* there needs to be support for Unicode here, - * unless we pretend that we can redefine the MD-5 - * algorithm for multi-byte characters (perhaps - * by adding every four 16-bit characters and - * shortening the sum to 32 bits). Otherwise - * I suggest performing MD-5 as if every character - * was two bytes--e.g., 0040 0025 = @%--but then - * how will an ordinary MD-5 sum be matched? - * There is no way to standardize text to something - * like UTF-8 before transformation; speed cost is - * utterly prohibitive. The JavaScript standard - * itself needs to look at this: it should start - * providing access to strings as preformed UTF-8 - * 8-bit unsigned value arrays. - */ -function md5blk(s) { - /* I figured global was faster. */ - let md5blks = [], - i; /* Andy King said do it this way. */ - for (i = 0; i < 64; i += 4) { - md5blks[i >> 2] = - s.charCodeAt(i) + - (s.charCodeAt(i + 1) << 8) + - (s.charCodeAt(i + 2) << 16) + - (s.charCodeAt(i + 3) << 24); - } - return md5blks; -} - -const hex_chr = "0123456789abcdef".split(""); - -function rhex(n) { - let s = "", - j = 0; - for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; - return s; -} - -function hex(x) { - for (let i = 0; i < x.length; i++) x[i] = rhex(x[i]); - return x.join(""); -} - -function md5(s) { - return hex(md51(s)); -} - -function add32(a, b) { - return (a + b) & 0xffffffff; -} - -export default md5; diff --git a/lib/profile.ts b/lib/profile.ts new file mode 100644 index 00000000..2ef123d9 --- /dev/null +++ b/lib/profile.ts @@ -0,0 +1,11 @@ +import crypto from "crypto"; + +export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) { + if (!email && !md5) return ""; + + if (email && !md5) { + md5 = crypto.createHash("md5").update(email).digest("hex"); + } + + return `https://www.gravatar.com/avatar/${md5}?s=160&d=identicon&r=PG`; +}; diff --git a/package.json b/package.json index 0e32277b..3fdd5f6e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@heroicons/react": "^1.0.4", "@jitsu/sdk-js": "^2.2.4", "@prisma/client": "^2.29.1", + "@radix-ui/react-avatar": "^0.0.15", "@radix-ui/react-collapsible": "^0.0.17", "@radix-ui/react-dialog": "^0.0.20", "@radix-ui/react-slider": "^0.0.17", diff --git a/pages/[user].tsx b/pages/[user].tsx index 876c0a16..f7a418d7 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -2,7 +2,7 @@ import { GetServerSideProps } from "next"; import Head from "next/head"; import Link from "next/link"; import prisma, { whereAndSelect } from "@lib/prisma"; -import Avatar from "../components/Avatar"; +import Avatar from "@components/Avatar"; import Theme from "@components/Theme"; import { ClockIcon, InformationCircleIcon, UserIcon } from "@heroicons/react/solid"; import React from "react"; @@ -105,7 +105,11 @@ export default function User(props): User {
- +

{props.user.name || props.user.username}

diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 2d96c38c..e4988b18 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -9,7 +9,7 @@ import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next"; import Head from "next/head"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import Avatar from "../../components/Avatar"; +import Avatar from "@components/Avatar"; import AvailableTimes from "../../components/booking/AvailableTimes"; import DatePicker from "../../components/booking/DatePicker"; import TimeOptions from "../../components/booking/TimeOptions"; @@ -140,7 +140,11 @@ export default function Type(props: InferGetServerSidePropsType
- +

{props.user.name}

@@ -161,7 +165,11 @@ export default function Type(props: InferGetServerSidePropsType - +

{props.user.name}

{props.eventType.title} diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index b9b19e7e..9145253d 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -12,7 +12,7 @@ import timezone from "dayjs/plugin/timezone"; import "react-phone-number-input/style.css"; import PhoneInput from "react-phone-number-input"; import { LocationType } from "../../lib/location"; -import Avatar from "../../components/Avatar"; +import Avatar from "@components/Avatar"; import Button from "../../components/ui/Button"; import Theme from "@components/Theme"; import { ReactMultiEmail } from "react-multi-email"; @@ -163,7 +163,11 @@ export default function Book(props: any): JSX.Element {
- +

{props.user.name}

{props.eventType.title} diff --git a/pages/api/auth/[...nextauth].tsx b/pages/api/auth/[...nextauth].tsx index e7755ef8..d00eaa45 100644 --- a/pages/api/auth/[...nextauth].tsx +++ b/pages/api/auth/[...nextauth].tsx @@ -20,7 +20,7 @@ export default NextAuth({ password: { label: "Password", type: "password", placeholder: "Your super secure password" }, }, async authorize(credentials) { - const user = await prisma.user.findFirst({ + const user = await prisma.user.findUnique({ where: { email: credentials.email, }, @@ -44,15 +44,13 @@ export default NextAuth({ username: user.username, email: user.email, name: user.name, - image: user.avatar, }; }, }), ], callbacks: { async jwt(token, user) { - // Add username to the token right after signin - if (user?.username) { + if (user) { token.id = user.id; token.username = user.username; } diff --git a/pages/api/me.ts b/pages/api/me.ts new file mode 100644 index 00000000..ca93db6b --- /dev/null +++ b/pages/api/me.ts @@ -0,0 +1,40 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "@lib/prisma"; +import { getSession } from "@lib/auth"; +import { defaultAvatarSrc } from "@lib/profile"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getSession({ req: req }); + if (!session) { + res.status(401).json({ message: "Not authenticated" }); + return; + } + + const user: User = await prisma.user.findUnique({ + where: { + id: session.user.id, + }, + select: { + id: true, + username: true, + name: true, + email: true, + bio: true, + timeZone: true, + weekStart: true, + startTime: true, + endTime: true, + bufferTime: true, + theme: true, + createdDate: true, + hideBranding: true, + avatar: true, + }, + }); + + user.avatar = user.avatar || defaultAvatarSrc({ email: user.email }); + + res.status(200).json({ + user, + }); +} diff --git a/pages/auth/logout.tsx b/pages/auth/logout.tsx index 25d17c40..a066723c 100644 --- a/pages/auth/logout.tsx +++ b/pages/auth/logout.tsx @@ -1,7 +1,6 @@ import Head from "next/head"; import Link from "next/link"; import { CheckIcon } from "@heroicons/react/outline"; -import { getSession, signOut } from "next-auth/client"; export default function Logout() { return ( @@ -44,14 +43,3 @@ export default function Logout() {

); } - -Logout.getInitialProps = async (context) => { - const { req } = context; - const session = await getSession({ req }); - - if (session) { - signOut({ redirect: false }); - } - - return { session: undefined }; -}; diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 4a059961..8476b46c 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -1,17 +1,18 @@ import { GetServerSideProps } from "next"; import Head from "next/head"; import { useEffect, useRef, useState } from "react"; -import prisma, { whereAndSelect } from "@lib/prisma"; +import prisma from "@lib/prisma"; import Modal from "../../components/Modal"; import Shell from "../../components/Shell"; import SettingsShell from "../../components/Settings"; -import Avatar from "../../components/Avatar"; +import Avatar from "@components/Avatar"; import { getSession } from "next-auth/client"; import Select from "react-select"; import TimezoneSelect from "react-timezone-select"; import { UsernameInput } from "../../components/ui/UsernameInput"; import ErrorAlert from "../../components/ui/alerts/Error"; import ImageUploader from "../../components/ImageUploader"; +import crypto from "crypto"; const themeOptions = [ { value: "light", label: "Light" }, @@ -28,7 +29,7 @@ export default function Settings(props) { const [selectedTheme, setSelectedTheme] = useState({ value: props.user.theme }); const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone }); const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart }); - const [imageSrc, setImageSrc] = useState(""); + const [imageSrc, setImageSrc] = useState(props.user.avatar); const [hasErrors, setHasErrors] = useState(false); const [errorMessage, setErrorMessage] = useState(""); @@ -156,9 +157,9 @@ export default function Settings(props) {
} + gravatarFallbackMd5={props.user.emailMd5} imageSrc={imageSrc} />

@@ -331,15 +332,30 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { redirect: { permanent: false, destination: "/auth/login" } }; } - const user = await whereAndSelect( - prisma.user.findFirst, - { + const user = await prisma.user.findUnique({ + where: { id: session.user.id, }, - ["id", "username", "name", "email", "bio", "avatar", "timeZone", "weekStart", "hideBranding", "theme"] - ); + select: { + id: true, + username: true, + name: true, + email: true, + bio: true, + avatar: true, + timeZone: true, + weekStart: true, + hideBranding: true, + theme: true, + }, + }); return { - props: { user }, // will be passed to the page component as props + props: { + user: { + ...user, + emailMd5: crypto.createHash("md5").update(user.email).digest("hex"), + }, + }, }; }; diff --git a/yarn.lock b/yarn.lock index da77b52e..e2ace3e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -780,6 +780,18 @@ "@radix-ui/react-polymorphic" "0.0.13" "@radix-ui/react-primitive" "0.0.15" +"@radix-ui/react-avatar@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-0.0.15.tgz#5cb9280bde8cc1bca27a816c4a0b631222c9308b" + integrity sha512-mZHHCMU7CODOpisqiimrvVCUtcFcQLtuaqQUBwR9dYXoMxjBN1XtQXUGdTvFpPFXd11C9b6i6Gjfvti6yD8Jcg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-layout-effect" "0.0.5" + "@radix-ui/react-collapsible@^0.0.17": version "0.0.17" resolved "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-0.0.17.tgz#d778ec1d5b7b4543fd4db1b3e4be96c74568d441"