Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions components/author-with-date.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Avatar } from '@/components/avatar'
import type { Author } from '@/lib/types'
import { getUserDisplayName } from '@/lib/user'
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
import Link from 'next/link'

Expand All @@ -9,23 +10,25 @@ type AuthorWithDateProps = {
}

export function AuthorWithDate({ author, date }: AuthorWithDateProps) {
const authorName = getUserDisplayName({ name: author.name })

return (
<div className="flex items-center gap-2 sm:gap-4">
<Link href={`/profile/${author.id}`}>
<a className="relative inline-flex">
<span className="hidden sm:flex">
<Avatar name={author.name!} src={author.image} />
<Avatar name={authorName} src={author.image} />
</span>
<span className="flex sm:hidden">
<Avatar name={author.name!} src={author.image} size="sm" />
<Avatar name={authorName} src={author.image} size="sm" />
</span>
</a>
</Link>
<div className="flex-1 text-sm sm:text-base">
<div>
<Link href={`/profile/${author.id}`}>
<a className="font-medium tracking-tight transition-colors hover:text-blue">
{author.name}
{authorName}
</a>
</Link>
</div>
Expand Down
21 changes: 21 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { serverEnv } from '@/env/server'
import { prisma } from '@/lib/prisma'
import { getUserDisplayName } from '@/lib/user'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { Role } from '@prisma/client'
import type { NextAuthOptions } from 'next-auth'
Expand Down Expand Up @@ -81,6 +82,26 @@ export const authOptions: NextAuthOptions = {
return false
}

if (!user.name?.trim()) {
const profileWithName = profile as
| { name?: string; login?: string; preferred_username?: string }
| undefined
const fallbackName = getUserDisplayName({
name:
profileWithName?.name ??
profileWithName?.preferred_username ??
profileWithName?.login,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullish coalescing skips fallback for empty string names

Medium Severity

The ?? (nullish coalescing) operator only falls through on null/undefined, not on empty strings. If profileWithName?.name is "" (which some OAuth providers return instead of null for unset names), the preferred_username and login fallbacks are skipped entirely. The || operator would correctly fall through for empty strings, preserving the intended fallback chain of name → preferred_username → login.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f48759. Configure here.

email: user.email,
})

if (user.id) {
await prisma.user.update({
where: { id: user.id },
data: { name: fallbackName },
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled DB error in signIn blocks authentication

Medium Severity

The prisma.user.update call inside the signIn callback isn't wrapped in a try-catch. If this database update fails (e.g., transient DB error), the unhandled exception propagates up and causes the entire sign-in to fail. Since the name update is a cosmetic enhancement, it shouldn't block user authentication.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f48759. Configure here.

}

return true
},
async session({ session, user }) {
Expand Down
23 changes: 23 additions & 0 deletions lib/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
type UserDisplayNameInput = {
name?: string | null
email?: string | null
}

export function getUserDisplayName({
name,
email,
}: UserDisplayNameInput): string {
const trimmedName = name?.trim()

if (trimmedName) {
return trimmedName
}

const emailPrefix = email?.split('@')[0]?.trim()

if (emailPrefix) {
return emailPrefix
}

return 'Beam user'
}
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
17 changes: 13 additions & 4 deletions pages/post/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from '@/components/menu'
import { InferQueryOutput, InferQueryPathAndInput, trpc } from '@/lib/trpc'
import type { NextPageWithAuthAndLayout } from '@/lib/types'
import { getUserDisplayName } from '@/lib/user'
import { useSession } from 'next-auth/react'
import Head from 'next/head'
import { useRouter } from 'next/router'
Expand All @@ -53,6 +54,10 @@ function getPostQueryPathAndInput(

const PostPage: NextPageWithAuthAndLayout = () => {
const { data: session } = useSession()
const currentUserDisplayName = getUserDisplayName({
name: session?.user?.name,
email: session?.user?.email,
})
const router = useRouter()
const utils = trpc.useContext()
const postQueryPathAndInput = getPostQueryPathAndInput(
Expand All @@ -70,7 +75,7 @@ const PostPage: NextPageWithAuthAndLayout = () => {
...previousPost,
likedBy: [
...previousPost.likedBy,
{ user: { id: session!.user.id, name: session!.user.name } },
{ user: { id: session!.user.id, name: currentUserDisplayName } },
],
})
}
Expand Down Expand Up @@ -272,11 +277,14 @@ const PostPage: NextPageWithAuthAndLayout = () => {
)}
<div className="flex items-start gap-2 sm:gap-4">
<span className="hidden sm:inline-block">
<Avatar name={session!.user.name} src={session!.user.image} />
<Avatar
name={currentUserDisplayName}
src={session!.user.image}
/>
</span>
<span className="inline-block sm:hidden">
<Avatar
name={session!.user.name}
name={currentUserDisplayName}
src={session!.user.image}
size="sm"
/>
Expand Down Expand Up @@ -365,6 +373,7 @@ function Comment({
comment: InferQueryOutput<'post.detail'>['comments'][number]
}) {
const { data: session } = useSession()
const authorName = getUserDisplayName({ name: comment.author.name })
const [isEditing, setIsEditing] = React.useState(false)
const [isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen] =
React.useState(false)
Expand All @@ -374,7 +383,7 @@ function Comment({
if (isEditing) {
return (
<div className="flex items-start gap-4">
<Avatar name={comment.author.name!} src={comment.author.image} />
<Avatar name={authorName} src={comment.author.image} />
<EditCommentForm
postId={postId}
comment={comment}
Expand Down
2 changes: 1 addition & 1 deletion server/routers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const userRouter = createProtectedRouter()
})
.mutation('edit', {
input: z.object({
name: z.string().min(1),
name: z.string().trim().min(1),
title: z.string().nullish(),
}),
async resolve({ ctx, input }) {
Expand Down
Loading