Skip to content

Commit 6528eef

Browse files
fix auth, notification, mention, database (#16)
* added support for database * snapdocs intro video * fix auth and support for mention * adds support for notificaiton --------- Co-authored-by: Vicky Thakor <[email protected]>
1 parent cee89a9 commit 6528eef

File tree

19 files changed

+1413
-22
lines changed

19 files changed

+1413
-22
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
3+
import { useEffect } from 'react'
4+
import { signOut } from 'next-auth/react'
5+
import { useRouter } from 'next/navigation'
6+
7+
export function InvalidSessionHandler() {
8+
const router = useRouter()
9+
10+
useEffect(() => {
11+
// Clear the invalid session and redirect to login
12+
signOut({
13+
redirect: false
14+
}).then(() => {
15+
router.push('/login?error=InvalidSession')
16+
})
17+
}, [router])
18+
19+
return (
20+
<div className="min-h-screen flex items-center justify-center bg-white dark:bg-[#191919]">
21+
<div className="text-center">
22+
<h1 className="text-2xl font-bold mb-2">Session Invalid</h1>
23+
<p className="text-gray-600 dark:text-gray-400 mb-4">
24+
Your session is no longer valid. Redirecting to login...
25+
</p>
26+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
27+
</div>
28+
</div>
29+
)
30+
}

app/(protected)/layout.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,32 @@ import { redirect } from "next/navigation"
22
import { getCurrentUser } from "@/lib/auth"
33
import { SnapDocsSidebar } from "@/components/layout/snapdocs-sidebar"
44
import { ClientLayout } from "./ClientLayout"
5+
import { getServerSession } from "next-auth"
6+
import { authOptions } from "@/lib/auth-config"
7+
import { InvalidSessionHandler } from "./InvalidSessionHandler"
58

69
interface ProtectedLayoutProps {
710
children: React.ReactNode
811
}
912

1013
export default async function ProtectedLayout({ children }: ProtectedLayoutProps) {
14+
// First check if there's a valid session
15+
const session = await getServerSession(authOptions)
16+
17+
if (!session) {
18+
// No valid session, redirect to login
19+
redirect("/login")
20+
}
21+
22+
// Then get the full user data
1123
const user = await getCurrentUser()
1224

1325
if (!user) {
14-
redirect("/login")
26+
// Session exists but user not found in database
27+
// This means the session is invalid (user was deleted or session is corrupted)
28+
console.error(`Session exists but user not found in database: ${session.user?.id}`)
29+
// Return a client component that will clear the session
30+
return <InvalidSessionHandler />
1531
}
1632

1733
return (

app/(protected)/workspace/[workspaceId]/page/[pageId]/PageEditorV2.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@ interface PageEditorProps {
6161
name?: string | null
6262
email?: string | null
6363
}
64+
workspaceMembers?: Array<{
65+
id: string
66+
name?: string | null
67+
email?: string | null
68+
}>
6469
}
6570

66-
export default function PageEditorV2({ page, initialContent, user }: PageEditorProps) {
71+
export default function PageEditorV2({ page, initialContent, user, workspaceMembers = [] }: PageEditorProps) {
6772
const router = useRouter()
6873
// const { isConnected, joinPage, leavePage } = useSocket() // Removed - using Yjs collaboration now
6974
const [title, setTitle] = useState(page.title || '')
@@ -316,6 +321,7 @@ export default function PageEditorV2({ page, initialContent, user }: PageEditorP
316321
autoSaveInterval={2000}
317322
userId={user?.id}
318323
user={user}
324+
workspaceMembers={workspaceMembers}
319325
/>
320326
</div>
321327
</div>

app/(protected)/workspace/[workspaceId]/page/[pageId]/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ export default async function PageEditorPage({ params }: PageEditorPageProps) {
7373
notFound()
7474
}
7575

76+
// Get all workspace members for mentions
77+
const workspaceMembers = await prisma.workspaceMember.findMany({
78+
where: {
79+
workspaceId: resolvedParams.workspaceId
80+
},
81+
include: {
82+
user: {
83+
select: {
84+
id: true,
85+
name: true,
86+
email: true
87+
}
88+
}
89+
}
90+
})
91+
7692
// Get page content from MongoDB
7793
const pageContent = await pageContentService.loadPageContent(resolvedParams.pageId)
7894

@@ -94,6 +110,7 @@ export default async function PageEditorPage({ params }: PageEditorPageProps) {
94110
page={transformedPage}
95111
initialContent={pageContent}
96112
user={user}
113+
workspaceMembers={workspaceMembers.map(m => m.user)}
97114
/>
98115
)
99116
} catch (error) {

app/api/auth/verify/route.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NextResponse } from "next/server"
2+
import { getServerSession } from "next-auth"
3+
import { authOptions } from "@/lib/auth-config"
4+
5+
export async function GET() {
6+
try {
7+
const session = await getServerSession(authOptions)
8+
9+
if (!session) {
10+
return NextResponse.json(
11+
{ error: "Unauthorized - Invalid or expired token" },
12+
{ status: 403 }
13+
)
14+
}
15+
16+
return NextResponse.json({
17+
authenticated: true,
18+
user: session.user
19+
})
20+
} catch (error) {
21+
console.error("Auth verification error:", error)
22+
return NextResponse.json(
23+
{ error: "Authentication verification failed" },
24+
{ status: 500 }
25+
)
26+
}
27+
}

app/api/pages/[pageId]/content/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { prisma } from '@/lib/db/prisma'
44
import { pageContentService } from '@/lib/services/page-content'
55
import { Block } from '@/types'
66
import { ensureMentionsPopulated } from '@/lib/utils/mentions'
7+
import { NotificationService } from '@/lib/services/notification'
78

89
// GET /api/pages/[pageId]/content - Get page content
910
export async function GET(
@@ -136,6 +137,14 @@ export async function PUT(
136137
// Save content immediately (optimized for real-time collaboration)
137138
const savedContent = await pageContentService.savePageContent(pageId, blocks, user.id)
138139

140+
// Process mentions and create notifications
141+
await NotificationService.processMentionsInContent(
142+
blocks,
143+
pageId,
144+
page.workspaceId,
145+
user.id
146+
)
147+
139148
// Update page metadata in PostgreSQL
140149
await prisma.page.update({
141150
where: { id: pageId },

app/login/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useEffect } from "react"
44
import { signIn, getSession, useSession } from "next-auth/react"
5-
import { useRouter } from "next/navigation"
5+
import { useRouter, useSearchParams } from "next/navigation"
66
import { Button } from "@/components/ui/button"
77
import { Input } from "@/components/ui/input"
88
import { Label } from "@/components/ui/label"
@@ -15,13 +15,20 @@ export default function LoginPage() {
1515
const [password, setPassword] = useState("")
1616
const [isLoading, setIsLoading] = useState(false)
1717
const router = useRouter()
18+
const searchParams = useSearchParams()
1819
const { data: session, status } = useSession()
1920

2021
useEffect(() => {
22+
// Check for error messages in URL
23+
const error = searchParams.get('error')
24+
if (error === 'InvalidSession') {
25+
toast.error('Your session has expired. Please login again.')
26+
}
27+
2128
if (status === "authenticated") {
2229
router.push("/dashboard")
2330
}
24-
}, [status, router])
31+
}, [status, router, searchParams])
2532

2633
const handleSubmit = async (e: React.FormEvent) => {
2734
e.preventDefault()

components/editor/BlockNoteEditor.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Block as BlockNoteBlock,
77
BlockNoteSchema,
88
defaultBlockSpecs,
9+
defaultInlineContentSpecs,
910
filterSuggestionItems
1011
} from '@blocknote/core'
1112
import {
@@ -29,6 +30,7 @@ import {
2930
cleanupCollaborationProvider
3031
} from '@/lib/collaboration/yjs-provider'
3132
import type YPartyKitProvider from 'y-partykit/provider'
33+
import { Mention } from './Mention'
3234

3335
// Dynamically import DatabaseBlock to avoid SSR issues
3436
const DatabaseBlock = dynamic(() => import('./DatabaseBlock'), {
@@ -52,6 +54,11 @@ interface BlockNoteEditorProps {
5254
email?: string | null
5355
}
5456
enableCollaboration?: boolean
57+
workspaceMembers?: Array<{
58+
id: string
59+
name?: string | null
60+
email?: string | null
61+
}>
5562
}
5663

5764
type SaveStatus = 'saved' | 'saving' | 'error' | 'unsaved'
@@ -67,7 +74,8 @@ export default function BlockNoteEditorComponent({
6774
showSaveStatus = true,
6875
userId,
6976
user,
70-
enableCollaboration = true
77+
enableCollaboration = true,
78+
workspaceMembers = []
7179
}: BlockNoteEditorProps) {
7280
const [saveStatus, setSaveStatus] = useState<SaveStatus>('saved')
7381
const autoSaveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
@@ -165,6 +173,10 @@ export default function BlockNoteEditorComponent({
165173
const schema = useMemo(() => {
166174
return BlockNoteSchema.create({
167175
blockSpecs: customBlockSpecs,
176+
inlineContentSpecs: {
177+
...defaultInlineContentSpecs,
178+
mention: Mention,
179+
},
168180
})
169181
}, [customBlockSpecs])
170182

@@ -275,6 +287,54 @@ export default function BlockNoteEditorComponent({
275287
[insertDatabaseItem]
276288
)
277289

290+
// Get mention menu items from workspace members
291+
const getMentionMenuItems = useCallback(
292+
(editor: any): DefaultReactSuggestionItem[] => {
293+
// Include the current user
294+
const allUsers = [
295+
...(user ? [user] : []),
296+
...workspaceMembers.filter(member => member.id !== user?.id)
297+
]
298+
299+
return allUsers.map((member) => ({
300+
title: member.name || member.email || 'Unknown User',
301+
onItemClick: async () => {
302+
editor.insertInlineContent([
303+
{
304+
type: "mention",
305+
props: {
306+
user: member.name || member.email || 'Unknown',
307+
userId: member.id,
308+
email: member.email || undefined
309+
},
310+
},
311+
" ", // add a space after the mention
312+
])
313+
314+
// Send notification for the mention
315+
if (member.id !== user?.id) {
316+
try {
317+
await fetch('/api/notifications/mention', {
318+
method: 'POST',
319+
headers: { 'Content-Type': 'application/json' },
320+
body: JSON.stringify({
321+
userId: member.id,
322+
pageId,
323+
workspaceId,
324+
message: `mentioned you in a document`
325+
})
326+
})
327+
} catch (error) {
328+
console.error('Failed to send mention notification:', error)
329+
}
330+
}
331+
},
332+
subtext: member.email || undefined,
333+
}))
334+
},
335+
[user, workspaceMembers, pageId, workspaceId]
336+
)
337+
278338
// Convert BlockNote blocks back to our storage format
279339
const convertFromBlockNoteFormat = useCallback((blocks: BlockNoteBlock[]): AppBlockType[] => {
280340
try {
@@ -441,6 +501,12 @@ export default function BlockNoteEditorComponent({
441501
filterSuggestionItems(getCustomSlashMenuItems(editor), query)
442502
}
443503
/>
504+
<SuggestionMenuController
505+
triggerCharacter="@"
506+
getItems={async (query) =>
507+
filterSuggestionItems(getMentionMenuItems(editor as any), query)
508+
}
509+
/>
444510
</BlockNoteView>
445511
</div>
446512

components/editor/Mention.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use client'
2+
3+
import { createReactInlineContentSpec } from '@blocknote/react'
4+
import { cn } from '@/lib/utils'
5+
6+
export const Mention = createReactInlineContentSpec(
7+
{
8+
type: "mention" as const,
9+
propSchema: {
10+
user: {
11+
default: "Unknown" as const
12+
},
13+
userId: {
14+
default: undefined,
15+
type: "string" as const
16+
},
17+
email: {
18+
default: undefined,
19+
type: "string" as const
20+
}
21+
} as const,
22+
content: "none" as const,
23+
},
24+
{
25+
render: (props) => {
26+
const { user, userId, email } = props.inlineContent.props
27+
28+
return (
29+
<span
30+
className={cn(
31+
"inline-flex items-center px-1.5 py-0.5 mx-0.5",
32+
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
33+
"rounded-md text-sm font-medium",
34+
"hover:bg-blue-200 dark:hover:bg-blue-900/50",
35+
"cursor-pointer transition-colors duration-150",
36+
"align-baseline"
37+
)}
38+
data-mention-user={user}
39+
data-mention-userid={userId}
40+
data-mention-email={email}
41+
contentEditable={false}
42+
>
43+
@{user}
44+
</span>
45+
)
46+
}
47+
}
48+
)

components/layout/snapdocs-sidebar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {
2424
Hash,
2525
Calendar,
2626
File,
27-
Circle
27+
Circle,
28+
Bell
2829
} from "lucide-react"
2930
import {
3031
DropdownMenu,
@@ -39,6 +40,7 @@ import { CreateWorkspaceModal } from "@/components/workspace/create-workspace-mo
3940
import { SearchDialog } from "@/components/search/SearchDialog"
4041
import { SettingsModal } from "@/components/settings/SettingsModal"
4142
import { AvatarUpload } from "@/components/ui/avatar-upload"
43+
import { NotificationButton } from "@/components/notifications/NotificationPanel"
4244
import { cn } from "@/lib/utils"
4345
import toast from "react-hot-toast"
4446

@@ -308,6 +310,7 @@ export function SnapDocsSidebar({ user }: SnapDocsSidebarProps) {
308310
}
309311
onClick={() => setShowSearchDialog(true)}
310312
/>
313+
<NotificationButton />
311314
<SidebarItem
312315
icon={<Settings />}
313316
label="Settings & members"

0 commit comments

Comments
 (0)