-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ feat(liveblock):Authenticate User with Real-time Features
- Loading branch information
1 parent
eaf1459
commit cf85742
Showing
10 changed files
with
1,215 additions
and
2,512 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { currentUser } from '@clerk/nextjs/server'; | ||
import { redirect } from 'next/navigation'; | ||
|
||
import { liveblocks } from '@/common/libs/liveblocks'; | ||
import { getUserColor } from '@/common/utils/getUserColor'; | ||
|
||
export async function POST(request: Request) { | ||
const clerkUser = await currentUser(); | ||
|
||
if (!clerkUser) redirect('/sign-in'); | ||
|
||
const { id, firstName, lastName, emailAddresses, imageUrl } = clerkUser; | ||
|
||
// Get the current user from your database | ||
const user = { | ||
id, | ||
info: { | ||
id, | ||
name: `${firstName} ${lastName}`, | ||
email: emailAddresses[0].emailAddress, | ||
avatar: imageUrl, | ||
color: getUserColor(id), | ||
}, | ||
}; | ||
|
||
// Identify the user and return the result | ||
const { status, body } = await liveblocks.identifyUser( | ||
{ | ||
userId: user.info.email, | ||
groupIds: [], | ||
}, | ||
{ userInfo: user.info } | ||
); | ||
|
||
return new Response(body, { status }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/* eslint-disable no-console */ | ||
'use server'; | ||
|
||
import { nanoid } from 'nanoid'; | ||
import { revalidatePath } from 'next/cache'; | ||
import { redirect } from 'next/navigation'; | ||
|
||
import { getAccessType } from '@/common/utils/getAccessType'; | ||
import { parseStringify } from '@/common/utils/parseStringify'; | ||
|
||
import { liveblocks } from '../liveblocks'; | ||
|
||
export const createDocument = async ({ | ||
userId, | ||
email, | ||
}: CreateDocumentParams) => { | ||
const roomId = nanoid(); | ||
|
||
try { | ||
const metadata = { | ||
creatorId: userId, | ||
email, | ||
title: 'Untitled', | ||
}; | ||
|
||
const usersAccesses: RoomAccesses = { | ||
[email]: ['room:write'], | ||
}; | ||
|
||
const room = await liveblocks.createRoom(roomId, { | ||
metadata, | ||
usersAccesses, | ||
defaultAccesses: [], | ||
}); | ||
|
||
revalidatePath('/'); | ||
|
||
return parseStringify(room); | ||
} catch (error) { | ||
console.log(`Error happened while creating a room: ${error}`); | ||
} | ||
}; | ||
|
||
export const getDocument = async ({ | ||
roomId, | ||
userId, | ||
}: { | ||
roomId: string; | ||
userId: string; | ||
}) => { | ||
try { | ||
const room = await liveblocks.getRoom(roomId); | ||
|
||
const hasAccess = Object.keys(room.usersAccesses).includes(userId); | ||
|
||
if (!hasAccess) { | ||
throw new Error('You do not have access to this document'); | ||
} | ||
|
||
return parseStringify(room); | ||
} catch (error) { | ||
console.log(`Error happened while getting a room: ${error}`); | ||
} | ||
}; | ||
|
||
export const updateDocument = async (roomId: string, title: string) => { | ||
try { | ||
const updatedRoom = await liveblocks.updateRoom(roomId, { | ||
metadata: { | ||
title, | ||
}, | ||
}); | ||
|
||
revalidatePath(`/documents/${roomId}`); | ||
|
||
return parseStringify(updatedRoom); | ||
} catch (error) { | ||
console.log(`Error happened while updating a room: ${error}`); | ||
} | ||
}; | ||
|
||
export const getDocuments = async (email: string) => { | ||
try { | ||
const rooms = await liveblocks.getRooms({ userId: email }); | ||
|
||
return parseStringify(rooms); | ||
} catch (error) { | ||
console.log(`Error happened while getting rooms: ${error}`); | ||
} | ||
}; | ||
|
||
export const updateDocumentAccess = async ({ | ||
roomId, | ||
email, | ||
userType, | ||
updatedBy, | ||
}: ShareDocumentParams) => { | ||
try { | ||
const usersAccesses: RoomAccesses = { | ||
[email]: getAccessType(userType) as AccessType, | ||
}; | ||
|
||
const room = await liveblocks.updateRoom(roomId, { | ||
usersAccesses, | ||
}); | ||
|
||
if (room) { | ||
const notificationId = nanoid(); | ||
|
||
await liveblocks.triggerInboxNotification({ | ||
userId: email, | ||
kind: '$documentAccess', | ||
subjectId: notificationId, | ||
activityData: { | ||
userType, | ||
title: `You have been granted ${userType} access to the document by ${updatedBy.name}`, | ||
updatedBy: updatedBy.name, | ||
avatar: updatedBy.avatar, | ||
email: updatedBy.email, | ||
}, | ||
roomId, | ||
}); | ||
} | ||
|
||
revalidatePath(`/documents/${roomId}`); | ||
return parseStringify(room); | ||
} catch (error) { | ||
console.log(`Error happened while updating a room access: ${error}`); | ||
} | ||
}; | ||
|
||
export const removeCollaborator = async ({ | ||
roomId, | ||
email, | ||
}: { | ||
roomId: string; | ||
email: string; | ||
}) => { | ||
try { | ||
const room = await liveblocks.getRoom(roomId); | ||
|
||
if (room.metadata.email === email) { | ||
throw new Error('You cannot remove yourself from the document'); | ||
} | ||
|
||
const updatedRoom = await liveblocks.updateRoom(roomId, { | ||
usersAccesses: { | ||
[email]: null, | ||
}, | ||
}); | ||
|
||
revalidatePath(`/documents/${roomId}`); | ||
return parseStringify(updatedRoom); | ||
} catch (error) { | ||
console.log(`Error happened while removing a collaborator: ${error}`); | ||
} | ||
}; | ||
|
||
export const deleteDocument = async (roomId: string) => { | ||
try { | ||
await liveblocks.deleteRoom(roomId); | ||
revalidatePath('/'); | ||
redirect('/'); | ||
} catch (error) { | ||
console.log(`Error happened while deleting a room: ${error}`); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* eslint-disable no-console */ | ||
'use server'; | ||
|
||
import { clerkClient } from '@clerk/nextjs/server'; | ||
|
||
import { parseStringify } from '@/common/utils/parseStringify'; | ||
|
||
import { liveblocks } from '../liveblocks'; | ||
|
||
export const getClerkUsers = async ({ userIds }: { userIds: string[] }) => { | ||
try { | ||
const { data } = await clerkClient.users.getUserList({ | ||
emailAddress: userIds, | ||
}); | ||
|
||
const users = data.map((user) => ({ | ||
id: user.id, | ||
name: `${user.firstName} ${user.lastName}`, | ||
email: user.emailAddresses[0].emailAddress, | ||
avatar: user.imageUrl, | ||
})); | ||
|
||
const sortedUsers = userIds.map((email) => | ||
users.find((user) => user.email === email) | ||
); | ||
|
||
return parseStringify(sortedUsers); | ||
} catch (error) { | ||
console.log(`Error fetching users: ${error}`); | ||
} | ||
}; | ||
|
||
export const getDocumentUsers = async ({ | ||
roomId, | ||
currentUser, | ||
text, | ||
}: { | ||
roomId: string; | ||
currentUser: string; | ||
text: string; | ||
}) => { | ||
try { | ||
const room = await liveblocks.getRoom(roomId); | ||
|
||
const users = Object.keys(room.usersAccesses).filter( | ||
(email) => email !== currentUser | ||
); | ||
|
||
if (text.length) { | ||
const lowerCaseText = text.toLowerCase(); | ||
|
||
const filteredUsers = users.filter((email: string) => | ||
email.toLowerCase().includes(lowerCaseText) | ||
); | ||
|
||
return parseStringify(filteredUsers); | ||
} | ||
|
||
return parseStringify(users); | ||
} catch (error) { | ||
console.log(`Error fetching document users: ${error}`); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { Liveblocks } from '@liveblocks/node'; | ||
|
||
export const liveblocks = new Liveblocks({ | ||
secret: process.env.LIVEBLOCKS_SECRET_KEY as string, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export const getAccessType = (userType: UserType) => { | ||
switch (userType) { | ||
case 'creator': | ||
return ['room:write']; | ||
case 'editor': | ||
return ['room:write']; | ||
case 'viewer': | ||
return ['room:read', 'room:presence:write']; | ||
default: | ||
return ['room:read', 'room:presence:write']; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Function to generate a random color in hex format, excluding specified colors | ||
export function getRandomColor() { | ||
const avoidColors = ['#000000', '#FFFFFF', '#8B4513']; // Black, White, Brown in hex format | ||
|
||
let randomColor; | ||
do { | ||
// Generate random RGB values | ||
const r = Math.floor(Math.random() * 256); // Random number between 0-255 | ||
const g = Math.floor(Math.random() * 256); | ||
const b = Math.floor(Math.random() * 256); | ||
|
||
// Convert RGB to hex format | ||
randomColor = `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`; | ||
} while (avoidColors.includes(randomColor)); | ||
|
||
return randomColor; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
export const brightColors = [ | ||
'#2E8B57', // Darker Neon Green | ||
'#FF6EB4', // Darker Neon Pink | ||
'#00CDCD', // Darker Cyan | ||
'#FF00FF', // Darker Neon Magenta | ||
'#FF007F', // Darker Bright Pink | ||
'#FFD700', // Darker Neon Yellow | ||
'#00CED1', // Darker Neon Mint Green | ||
'#FF1493', // Darker Neon Red | ||
'#00CED1', // Darker Bright Aqua | ||
'#FF7F50', // Darker Neon Coral | ||
'#9ACD32', // Darker Neon Lime | ||
'#FFA500', // Darker Neon Orange | ||
'#32CD32', // Darker Neon Chartreuse | ||
'#ADFF2F', // Darker Neon Yellow Green | ||
'#DB7093', // Darker Neon Fuchsia | ||
'#00FF7F', // Darker Spring Green | ||
'#FFD700', // Darker Electric Lime | ||
'#FF007F', // Darker Bright Magenta | ||
'#FF6347', // Darker Neon Vermilion | ||
]; | ||
|
||
export function getUserColor(userId: string) { | ||
let sum = 0; | ||
for (let i = 0; i < userId.length; i++) { | ||
sum += userId.charCodeAt(i); | ||
} | ||
|
||
const colorIndex = sum % brightColors.length; | ||
return brightColors[colorIndex]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value)); |