Skip to content

Commit

Permalink
✨ feat(liveblock):Authenticate User with Real-time Features
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackishGreen33 committed Oct 6, 2024
1 parent eaf1459 commit cf85742
Show file tree
Hide file tree
Showing 10 changed files with 1,215 additions and 2,512 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"jsm-editor": "^0.0.12",
"lexical": "^0.18.0",
"lucide-react": "^0.447.0",
"nanoid": "^5.0.7",
"next": "14.2.14",
"react": "^18",
"react-dom": "^18",
Expand Down
3,393 changes: 881 additions & 2,512 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions src/app/api/liveblocks-auth/route.ts
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 });
}
167 changes: 167 additions & 0 deletions src/common/libs/actions/room.actions.ts
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}`);
}
};
63 changes: 63 additions & 0 deletions src/common/libs/actions/user.actions.ts
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}`);
}
};
5 changes: 5 additions & 0 deletions src/common/libs/liveblocks.ts
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,
});
12 changes: 12 additions & 0 deletions src/common/utils/getAccessType.ts
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'];
}
};
17 changes: 17 additions & 0 deletions src/common/utils/getRandomColor.ts
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;
}
31 changes: 31 additions & 0 deletions src/common/utils/getUserColor.ts
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];
}
2 changes: 2 additions & 0 deletions src/common/utils/parseStringify.ts
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));

0 comments on commit cf85742

Please sign in to comment.