Added end to end encryption

This commit is contained in:
Aslan 2026-01-13 17:34:39 -05:00
parent 5733975aa0
commit 6f292756ed
34 changed files with 682 additions and 69 deletions

View file

@ -41,10 +41,12 @@ const postRegister = async (request: FastifyRequest, reply: FastifyReply) => {
const postLogin = async (request: FastifyRequest, reply: FastifyReply) => {
const { username, password } = request.body as IPostLoginRequest;
const userAgent = request.headers["user-agent"];
const session = await loginUser({
username: username,
password: password,
userAgent: userAgent ?? "",
});
if (!session) {
@ -85,6 +87,7 @@ const getRefresh = async (request: FastifyRequest, reply: FastifyReply) => {
id: refresh[0].id,
ownerId: refresh[0].userId,
token: refresh[1],
storageSecret: refresh[0].storageSecret,
} as IGetRefreshResponseSuccess;
};

View file

@ -35,6 +35,7 @@ interface IGetRefreshResponseSuccess {
id: string;
ownerId: string;
token: string;
storageSecret: string;
}
interface IGetRefreshResponseError {

View file

@ -164,6 +164,7 @@ const getMessages = async (request: FastifyRequest, reply: FastifyReply) => {
messages: messages.map((message) => ({
id: message.id,
text: message.text,
iv: message.iv,
edited: message.edited,
ownerId: message.ownerId,
creationDate: message.creationDate.getTime(),

View file

@ -79,6 +79,7 @@ interface IGetMessagesResponseSuccess {
interface IGetMessagesResponseMessage {
id: string;
text: string;
iv: string;
edited: boolean;
ownerId: string;
creationDate: number;

View file

@ -29,17 +29,21 @@ import type {
IPostCreateInviteRequest,
IPostCreateInviteResponseError,
IPostCreateInviteResponseSuccess,
IDeleteMemberParams,
IDeleteMemberResponseError,
IDeleteMemberResponseSuccess,
} from "./types.js";
import {
getCommunityById,
createCommunityAuth,
updateCommunityByIdAuth,
deleteCommunityByIdAuth,
getCommunityChannelsByIdAuth,
getCommunityMembersByIdAuth,
getCommunityRolesByIdAuth,
getCommunityInvitesByIdAuth,
createInviteAuth,
deleteCommunityByIdAuth,
deleteMemberByIdAuth,
} from "../../services/community/community.js";
import { API_ERROR } from "../errors.js";
import type { ICreateInvite } from "../../services/community/types.js";
@ -318,6 +322,35 @@ const postCreateInvite = async (
} as IPostCreateInviteResponseSuccess;
};
const deleteCommunityMember = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const { id, memberId } = request.params as IDeleteMemberParams;
const authHeader = request.headers["authorization"];
const community = await deleteMemberByIdAuth(id, memberId, authHeader);
if (!community) {
reply.status(404);
return {
id: id,
error: API_ERROR.NOT_FOUND,
} as IDeleteMemberResponseError;
}
if (community === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
id: id,
error: API_ERROR.ACCESS_DENIED,
} as IDeleteMemberResponseError;
}
return {
id: community.id,
userId: memberId,
} as IDeleteMemberResponseSuccess;
};
export {
getCommunity,
postCreateCommunity,
@ -328,4 +361,5 @@ export {
getRoles,
getInvites,
postCreateInvite,
deleteCommunityMember,
};

View file

@ -11,6 +11,7 @@ const communityRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id/roles`, controller.getRoles);
fastify.get(`/:id/invites`, controller.getInvites);
fastify.post(`/:id/invite`, controller.postCreateInvite);
fastify.delete(`/:id/members/:memberId`, controller.deleteCommunityMember);
};
export { communityRoutes };

View file

@ -158,6 +158,21 @@ interface IPostCreateInviteResponseSuccess {
inviteId: string;
}
interface IDeleteMemberParams {
id: string;
memberId: string;
}
interface IDeleteMemberResponseError {
id: string;
error: API_ERROR;
}
interface IDeleteMemberResponseSuccess {
id: string;
userId: string;
}
export {
type ICommunity,
type IGetCommunityParams,
@ -193,4 +208,7 @@ export {
type IPostCreateInviteRequest,
type IPostCreateInviteResponseError,
type IPostCreateInviteResponseSuccess,
type IDeleteMemberParams,
type IDeleteMemberResponseError,
type IDeleteMemberResponseSuccess,
};

View file

@ -45,6 +45,7 @@ const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
return {
id: message.id,
text: message.text,
iv: message.iv,
editHistory: message.editHistory,
edited: message.edited,
ownerId: message.ownerId,
@ -71,6 +72,7 @@ const postCreateMessage = async (
return {
id: message.id,
text: message.text,
iv: message.iv,
editHistory: message.editHistory,
edited: message.edited,
ownerId: message.ownerId,

View file

@ -3,6 +3,7 @@ import type { API_ERROR } from "../errors.js";
interface IMessage {
id: string;
text: string;
iv: string;
editHistory: string[];
edited: boolean;
ownerId: string;
@ -23,6 +24,7 @@ interface IGetMessageResponseSuccess extends IMessage {}
interface IPostCreateMessageRequest {
text: string;
iv: string;
channelId: string;
}

View file

@ -36,7 +36,10 @@ const getSession = async (request: FastifyRequest, reply: FastifyReply) => {
return {
id: session.id,
userId: session.userId,
name: session.name,
userAgent: session.userAgent,
creationDate: session.creationDate.getTime(),
refreshDate: session.refreshDate.getTime(),
} as IGetSessionResponseSuccess;
};

View file

@ -3,7 +3,10 @@ import type { API_ERROR } from "../errors.js";
interface ISession {
id: string;
userId: string;
name: string;
userAgent: string;
creationDate: number;
refreshDate: number;
}
interface IGetSessionParams {

View file

@ -90,6 +90,10 @@ interface IGetSessionsResponseSuccess {
interface IGetSessionsResponseSession {
id: string;
userId: string;
name: string;
userAgent: string;
creationDate: number;
refreshDate: number;
}
interface IGetCommunitiesParams {

View file

@ -185,6 +185,10 @@ const getSessions = async (request: FastifyRequest, reply: FastifyReply) => {
sessions: sessions.map((session) => ({
id: session.id,
userId: session.userId,
name: session.name,
userAgent: session.userAgent,
creationDate: session.creationDate.getTime(),
refreshDate: session.refreshDate.getTime(),
})),
} as IGetSessionsResponseSuccess;
};

View file

@ -17,6 +17,7 @@ import { roleRoutes } from "./controllers/role/routes.js";
import { inviteRoutes } from "./controllers/invite/routes.js";
import { messageRoutes } from "./controllers/message/routes.js";
import { websocketRoutes } from "./controllers/websocket/routes.js";
import { initializeCommunitiesWithReadableUsersCache } from "./services/user/user.js";
const app = Fastify({
logger: true,
@ -25,6 +26,7 @@ const app = Fastify({
app.register(cors, {
origin: "http://localhost:3000",
credentials: true,
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
});
app.register(cookie, { secret: getCookieSecret() });
@ -46,3 +48,5 @@ app.listen({ port: config.port }, (err, address) => {
if (err) throw err;
console.log(`Server is now listening on ${address}`);
});
initializeCommunitiesWithReadableUsersCache();

View file

@ -1,8 +1,10 @@
import { UAParser } from "ua-parser-js";
import type { User, Session } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js";
import {
createSessionCookie,
createToken,
getRandomBytesHex,
hashPassword,
verifyPassword,
} from "./helpers.js";
@ -45,6 +47,9 @@ const registerUser = async (
};
const loginUser = async (login: IUserLogin): Promise<Session | null> => {
const uaParser = new UAParser(login.userAgent);
const sessionName = `${uaParser.getBrowser()} on ${uaParser.getOS()}`;
const user = await getDB().user.findUnique({
where: { username: login.username },
});
@ -69,6 +74,9 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
data: {
cookie: createSessionCookie(),
userId: user.id,
name: sessionName,
userAgent: login.userAgent,
storageSecret: `${getRandomBytesHex(32)};${getRandomBytesHex(12)}`,
},
});
};
@ -90,6 +98,15 @@ const refreshSession = async (
return null;
}
await getDB().session.update({
where: {
id: session.id,
},
data: {
refreshDate: new Date(),
},
});
return [session, createToken(session.id)];
};

View file

@ -14,8 +14,12 @@ const getCookieSecret = (): string => {
return process.env.COOKIE_SECRET || "";
};
const getRandomBytesHex = (amount: number) => {
return crypto.randomBytes(amount).toString("hex");
};
const createSessionCookie = () => {
return crypto.randomBytes(32).toString("hex");
return getRandomBytesHex(32);
};
const createToken = (sessionId: string) => {
@ -152,19 +156,15 @@ const isUserOwnerOrAdmin = async (
};
const getUserPermissions = async (
user: User | null,
community: Community | null,
userId: string,
communityId: string,
): Promise<PERMISSION[]> => {
if (!user || !community) {
return [];
}
const roles = await getDB().role.findMany({
where: {
communityId: community.id,
communityId: communityId,
users: {
some: {
id: user.id,
id: userId,
},
},
},
@ -184,15 +184,15 @@ const getUserPermissions = async (
};
const userHasPermissions = async (
user: User | null,
community: Community | null,
userId: string | undefined,
communityId: string | undefined,
requiredPermissions: PERMISSION[],
): Promise<boolean> => {
if (!user || !community) {
if (!userId || !communityId) {
return false;
}
const userPermissions = await getUserPermissions(user, community);
const userPermissions = await getUserPermissions(userId, communityId);
return requiredPermissions.every((requiredPermission) =>
userPermissions.includes(requiredPermission),
@ -208,7 +208,9 @@ const isUserAllowed = async (
if (await isUserOwnerOrAdmin(user, ownerCheck)) {
return true;
}
if (await userHasPermissions(user, community, requiredPermissions)) {
if (
await userHasPermissions(user?.id, community?.id, requiredPermissions)
) {
return true;
}
@ -240,6 +242,7 @@ const isUserInCommunity = async (
export {
getJwtSecret,
getCookieSecret,
getRandomBytesHex,
createSessionCookie,
createToken,
verifyToken,

View file

@ -22,6 +22,7 @@ interface IUserRegistration {
interface IUserLogin {
username: string;
password: string;
userAgent: string;
}
interface IOwnerCheck {

View file

@ -4,6 +4,9 @@ import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js";
import { getCommunityById } from "../community/community.js";
import { getUserIdsInCommunity } from "../user/user.js";
import { SocketMessageTypes } from "../websocket/types.js";
import { sendMessageToUsersWS } from "../websocket/websocket.js";
import type { ICreateChannel, IUpdateChannel } from "./types.js";
const getChannelById = async (id: string): Promise<Channel | null> => {
@ -37,11 +40,22 @@ const getChannelByIdAuth = async (
};
const createChannel = async (create: ICreateChannel): Promise<Channel> => {
return await getDB().channel.create({
const createdChannel = await getDB().channel.create({
data: {
...create,
},
});
const userIds = await getUserIdsInCommunity(createdChannel.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_CHANNELS,
payload: {
communityId: createdChannel.communityId,
},
});
return createdChannel;
};
const createChannelAuth = async (
@ -71,7 +85,7 @@ const updateChannelById = async (
id: string,
update: IUpdateChannel,
): Promise<Channel | null> => {
return await getDB().channel.update({
const updatedChannel = await getDB().channel.update({
where: {
id: id,
},
@ -79,6 +93,17 @@ const updateChannelById = async (
...update,
},
});
const userIds = await getUserIdsInCommunity(updatedChannel.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_CHANNELS,
payload: {
communityId: updatedChannel.communityId,
},
});
return updatedChannel;
};
const updateChannelByIdAuth = async (
@ -107,9 +132,20 @@ const updateChannelByIdAuth = async (
};
const deleteChannelById = async (id: string): Promise<Channel | null> => {
return await getDB().channel.delete({
const deletedChannel = await getDB().channel.delete({
where: { id: id },
});
const userIds = await getUserIdsInCommunity(deletedChannel.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_CHANNELS,
payload: {
communityId: deletedChannel.communityId,
},
});
return deletedChannel;
};
const deleteChannelByIdAuth = async (

View file

@ -7,6 +7,9 @@ import {
isUserOwnerOrAdmin,
} from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js";
import { getUserIdsInCommunity } from "../user/user.js";
import { SocketMessageTypes } from "../websocket/types.js";
import { sendMessageToUsersWS } from "../websocket/websocket.js";
import type {
ICreateCommunity,
IUpdateCommunity,
@ -287,6 +290,7 @@ const createInviteAuth = async (
const community = await getCommunityById(id);
if (
!authUser ||
!(await isUserAllowed(
authUser,
{
@ -294,8 +298,7 @@ const createInviteAuth = async (
},
community,
[PERMISSION.INVITES_CREATE],
)) ||
!authUser
))
) {
return API_ERROR.ACCESS_DENIED;
}
@ -303,6 +306,60 @@ const createInviteAuth = async (
return await createInvite(id, authUser.id, createInviteData);
};
const deleteMemberById = async (
id: string,
memberId: string,
): Promise<Community> => {
const updatedCommunity = await getDB().community.update({
where: {
id: id,
},
data: {
members: {
disconnect: {
id: memberId,
},
},
},
});
const userIds = await getUserIdsInCommunity(updatedCommunity.id);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_MEMBERS,
payload: {
communityId: updatedCommunity.id,
},
});
return updatedCommunity;
};
const deleteMemberByIdAuth = async (
id: string,
memberId: string,
authHeader: string | undefined,
): Promise<Community | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const community = await getCommunityById(id);
if (
!(await isUserAllowed(
authUser,
{
community: community,
},
community,
[PERMISSION.MEMBERS_KICK],
)) &&
authUser?.id !== memberId
) {
return API_ERROR.ACCESS_DENIED;
}
return await deleteMemberById(id, memberId);
};
export {
getCommunityById,
createCommunity,
@ -321,4 +378,6 @@ export {
getCommunityInvitesByIdAuth,
createInvite,
createInviteAuth,
deleteMemberById,
deleteMemberByIdAuth,
};

View file

@ -8,6 +8,9 @@ import { getDB } from "../../store/store.js";
import { getCommunityById } from "../community/community.js";
import { PERMISSION } from "../auth/permission.js";
import { API_ERROR } from "../../controllers/errors.js";
import { getUserIdsInCommunity } from "../user/user.js";
import { sendMessageToUsersWS } from "../websocket/websocket.js";
import { SocketMessageTypes } from "../websocket/types.js";
const getInviteById = async (id: string): Promise<Invite | null> => {
return await getDB().invite.findUnique({
@ -63,7 +66,7 @@ const acceptInviteById = async (
},
});
return await getDB().community.update({
const updatedCommunity = await getDB().community.update({
where: {
id: invite.communityId,
},
@ -75,6 +78,17 @@ const acceptInviteById = async (
},
},
});
const userIds = await getUserIdsInCommunity(updatedCommunity.id);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_MEMBERS,
payload: {
communityId: updatedCommunity.id,
},
});
return updatedCommunity;
};
const acceptInviteByIdAuth = async (

View file

@ -10,8 +10,9 @@ import {
import { PERMISSION } from "../auth/permission.js";
import { getChannelById } from "../channel/channel.js";
import { getCommunityById } from "../community/community.js";
import { getUserIdsInCommunityReadMessagesPermission } from "../user/user.js";
import { SocketMessageTypes } from "../websocket/types.js";
import { sendMessageToUsers } from "../websocket/websocket.js";
import { sendMessageToUsersWS } from "../websocket/websocket.js";
import type { ICreateMessage, IUpdateMessage } from "./types.js";
const getMessageById = async (id: string): Promise<Message | null> => {
@ -57,27 +58,17 @@ const createMessage = async (
},
});
const usersInCommunity = await getDB().user.findMany({
select: {
id: true,
},
where: {
communities: {
some: {
id: communityId,
},
},
},
});
const userIds = usersInCommunity.map((user) => user.id);
const userIds =
await getUserIdsInCommunityReadMessagesPermission(communityId);
sendMessageToUsers(userIds, {
type: SocketMessageTypes.NEW_MESSAGE,
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.SET_MESSAGE,
payload: {
channelId: message.channelId,
message: {
id: message.id,
text: message.text,
iv: message.iv ?? "",
edited: message.edited,
ownerId: message.ownerId,
creationDate: message.creationDate.getTime(),
@ -116,6 +107,7 @@ const createMessageAuth = async (
const updateMessageById = async (
id: string,
communityId: string,
update: IUpdateMessage,
): Promise<Message | null> => {
const message = await getMessageById(id);
@ -125,7 +117,7 @@ const updateMessageById = async (
const newEditHistory = [...message.editHistory, message.text];
return await getDB().message.update({
const updatedMessage = await getDB().message.update({
where: {
id: id,
},
@ -135,6 +127,26 @@ const updateMessageById = async (
edited: true,
},
});
const userIds =
await getUserIdsInCommunityReadMessagesPermission(communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.SET_MESSAGE,
payload: {
channelId: updatedMessage.channelId,
message: {
id: updatedMessage.id,
text: updatedMessage.text,
iv: updatedMessage.iv ?? "",
edited: updatedMessage.edited,
ownerId: updatedMessage.ownerId,
creationDate: updatedMessage.creationDate.getTime(),
},
},
});
return updatedMessage;
};
const updateMessageByIdAuth = async (
@ -148,6 +160,7 @@ const updateMessageByIdAuth = async (
const community = await getCommunityById(channel?.communityId ?? "");
if (
!community ||
!(await isUserOwnerOrAdmin(authUser, {
message: message,
})) ||
@ -156,13 +169,29 @@ const updateMessageByIdAuth = async (
return API_ERROR.ACCESS_DENIED;
}
return await updateMessageById(id, update);
return await updateMessageById(id, community?.id, update);
};
const deleteMessageById = async (id: string): Promise<Message | null> => {
return await getDB().message.delete({
const deleteMessageById = async (
id: string,
communityId: string,
): Promise<Message | null> => {
const deletedMessage = await getDB().message.delete({
where: { id: id },
});
const userIds =
await getUserIdsInCommunityReadMessagesPermission(communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.DELETE_MESSAGE,
payload: {
channelId: deletedMessage.channelId,
messageId: deletedMessage.id,
},
});
return deletedMessage;
};
const deleteMessageByIdAuth = async (
@ -175,6 +204,7 @@ const deleteMessageByIdAuth = async (
const community = await getCommunityById(channel?.communityId ?? "");
if (
!community ||
!(await isUserAllowed(
authUser,
{
@ -187,7 +217,7 @@ const deleteMessageByIdAuth = async (
return API_ERROR.ACCESS_DENIED;
}
return await deleteMessageById(id);
return await deleteMessageById(id, community.id);
};
export {

View file

@ -1,5 +1,6 @@
interface ICreateMessage {
text: string;
iv: string;
channelId: string;
}

View file

@ -4,6 +4,13 @@ import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js";
import { getCommunityById } from "../community/community.js";
import {
getUserIdsInCommunity,
getUserIdsWithRole,
updateCommunitiesWithReadableUsersCache,
} from "../user/user.js";
import { SocketMessageTypes } from "../websocket/types.js";
import { sendMessageToUsersWS } from "../websocket/websocket.js";
import type { ICreateRole, IUpdateRole } from "./types.js";
const getRoleById = async (id: string): Promise<Role | null> => {
@ -37,11 +44,22 @@ const getRoleByIdAuth = async (
};
const createRole = async (create: ICreateRole): Promise<Role> => {
return await getDB().role.create({
const createdRole = await getDB().role.create({
data: {
...create,
},
});
const userIds = await getUserIdsInCommunity(createdRole.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_ROLES,
payload: {
communityId: createdRole.communityId,
},
});
return createdRole;
};
const createRoleAuth = async (
@ -71,7 +89,7 @@ const updateRoleById = async (
id: string,
update: IUpdateRole,
): Promise<Role | null> => {
return await getDB().role.update({
const updatedRole = await getDB().role.update({
where: {
id: id,
},
@ -79,6 +97,30 @@ const updateRoleById = async (
...update,
},
});
if (!update.permissions) {
return updatedRole;
}
const usersWithRole = await getUserIdsWithRole(id);
usersWithRole.forEach((userId) => {
updateCommunitiesWithReadableUsersCache(
updatedRole.communityId,
userId,
);
});
const userIds = await getUserIdsInCommunity(updatedRole.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_ROLES,
payload: {
communityId: updatedRole.communityId,
},
});
return updatedRole;
};
const updateRoleByIdAuth = async (
@ -107,9 +149,29 @@ const updateRoleByIdAuth = async (
};
const deleteRoleById = async (id: string): Promise<Role | null> => {
return await getDB().role.delete({
const usersWithRole = await getUserIdsWithRole(id);
const deletedRole = await getDB().role.delete({
where: { id: id },
});
usersWithRole.forEach((userId) => {
updateCommunitiesWithReadableUsersCache(
deletedRole.communityId,
userId,
);
});
const userIds = await getUserIdsInCommunity(deletedRole.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_ROLES,
payload: {
communityId: deletedRole.communityId,
},
});
return deletedRole;
};
const deleteRoleByIdAuth = async (
@ -140,7 +202,7 @@ const assignRoleById = async (
id: string,
userId: string,
): Promise<Role | null> => {
return await getDB().role.update({
const updatedRole = await getDB().role.update({
where: {
id: id,
},
@ -152,6 +214,26 @@ const assignRoleById = async (
},
},
});
const usersWithRole = await getUserIdsWithRole(id);
usersWithRole.forEach((userId) => {
updateCommunitiesWithReadableUsersCache(
updatedRole.communityId,
userId,
);
});
const userIds = await getUserIdsInCommunity(updatedRole.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_MEMBERS,
payload: {
communityId: updatedRole.communityId,
},
});
return updatedRole;
};
const assignRoleByIdAuth = async (
@ -183,7 +265,7 @@ const unassignRoleById = async (
id: string,
userId: string,
): Promise<Role | null> => {
return await getDB().role.update({
const updatedRole = await getDB().role.update({
where: {
id: id,
},
@ -195,6 +277,26 @@ const unassignRoleById = async (
},
},
});
const usersWithRole = await getUserIdsWithRole(id);
usersWithRole.forEach((userId) => {
updateCommunitiesWithReadableUsersCache(
updatedRole.communityId,
userId,
);
});
const userIds = await getUserIdsInCommunity(updatedRole.communityId);
sendMessageToUsersWS(userIds, {
type: SocketMessageTypes.UPDATE_MEMBERS,
payload: {
communityId: updatedRole.communityId,
},
});
return updatedRole;
};
const unassignRoleByIdAuth = async (

View file

@ -3,10 +3,82 @@ import type {
Session,
Community,
} from "../../generated/prisma/client.js";
import { getUserFromAuth, isUserOwnerOrAdmin } from "../auth/helpers.js";
import {
getUserFromAuth,
getUserPermissions,
isUserOwnerOrAdmin,
} from "../auth/helpers.js";
import { getDB } from "../../store/store.js";
import { API_ERROR } from "../../controllers/errors.js";
import type { ICreateUser, IUpdateUser } from "./types.js";
import { PERMISSION } from "../auth/permission.js";
const communitiesWithReadableUsersCache = new Map<string, Set<string>>();
const initializeCommunitiesWithReadableUsersCache = async () => {
const allCommunities = await getDB().community.findMany({
select: {
id: true,
ownerId: true,
members: {
select: {
id: true,
roles: {
select: {
communityId: true,
permissions: true,
},
},
},
},
},
});
for (const community of allCommunities) {
for (const member of community.members) {
let usersInCommunity = communitiesWithReadableUsersCache.get(
community.id,
);
if (!usersInCommunity) {
usersInCommunity = new Set<string>();
}
const hasReadRole = member.roles
.filter((role) => role.communityId === community.id)
.some((role) =>
role.permissions.includes(PERMISSION.MESSAGES_READ),
);
if (member.id === community.ownerId || hasReadRole) {
usersInCommunity.add(member.id);
}
communitiesWithReadableUsersCache.set(
community.id,
usersInCommunity,
);
}
}
};
const updateCommunitiesWithReadableUsersCache = async (
communityId: string,
userId: string,
) => {
let usersInCommunity = communitiesWithReadableUsersCache.get(communityId);
if (!usersInCommunity) {
usersInCommunity = new Set<string>();
communitiesWithReadableUsersCache.set(communityId, usersInCommunity);
}
const userPermissions = await getUserPermissions(userId, communityId);
if (userPermissions.includes(PERMISSION.MESSAGES_READ)) {
usersInCommunity.add(userId);
} else {
usersInCommunity.delete(userId);
}
};
const getLoggedUserAuth = async (
authHeader: string | undefined,
@ -182,7 +254,57 @@ const getUserCommunitiesByIdAuth = async (
return communities;
};
const getUserIdsInCommunity = async (
communityId: string,
): Promise<string[]> => {
const usersInCommunity = await getDB().user.findMany({
select: {
id: true,
},
where: {
communities: {
some: {
id: communityId,
},
},
},
});
return usersInCommunity.map((user) => user.id);
};
const getUserIdsInCommunityReadMessagesPermission = async (
communityId: string,
): Promise<string[]> => {
const userIds = communitiesWithReadableUsersCache.get(communityId);
if (!userIds) {
return [];
}
return [...userIds];
};
const getUserIdsWithRole = async (id: string): Promise<string[]> => {
const usersWithRole = await getDB().user.findMany({
select: {
id: true,
},
where: {
roles: {
some: {
id: id,
},
},
},
});
return usersWithRole.map((user) => user.id);
};
export {
communitiesWithReadableUsersCache,
initializeCommunitiesWithReadableUsersCache,
updateCommunitiesWithReadableUsersCache,
getLoggedUserAuth,
getUserById,
getUserByIdAuth,
@ -196,4 +318,7 @@ export {
getUserSessionsByIdAuth,
getUserCommunitiesById,
getUserCommunitiesByIdAuth,
getUserIdsInCommunity,
getUserIdsInCommunityReadMessagesPermission,
getUserIdsWithRole,
};

View file

@ -11,9 +11,12 @@ enum SocketRequestTypes {
}
enum SocketMessageTypes {
NEW_ANNOUNCEMENT = "NEW_ANNOUNCEMENT",
NEW_MESSAGE = "NEW_MESSAGE",
NEW_CHANNEL = "NEW_CHANNEL",
ANNOUNCEMENT = "ANNOUNCEMENT",
SET_MESSAGE = "SET_MESSAGE",
DELETE_MESSAGE = "DELETE_MESSAGE",
UPDATE_CHANNELS = "UPDATE_CHANNELS",
UPDATE_ROLES = "UPDATE_ROLES",
UPDATE_MEMBERS = "UPDATE_MEMBERS",
}
type SocketRequest = {
@ -22,25 +25,42 @@ type SocketRequest = {
type SocketMessage =
| {
type: SocketMessageTypes.NEW_ANNOUNCEMENT;
type: SocketMessageTypes.ANNOUNCEMENT;
payload: {
title: string;
description: string;
};
}
| {
type: SocketMessageTypes.NEW_MESSAGE;
type: SocketMessageTypes.SET_MESSAGE;
payload: {
channelId: string;
message: IGetMessagesResponseMessage;
};
}
| {
type: SocketMessageTypes.NEW_CHANNEL;
type: SocketMessageTypes.DELETE_MESSAGE;
payload: {
channelId: string;
messageId: string;
};
}
| {
type: SocketMessageTypes.UPDATE_CHANNELS;
payload: {
communityId: string;
};
}
| {
type: SocketMessageTypes.UPDATE_ROLES;
payload: {
communityId: string;
};
}
| {
type: SocketMessageTypes.UPDATE_MEMBERS;
payload: {
id: string;
communityId: string;
name: string;
};
};

View file

@ -53,7 +53,7 @@ const onMessageWsHandler = (connection: ISocketConnection) => {
};
};
const sendMessageToUser = (userId: string, message: SocketMessage) => {
const sendMessageToUserWS = (userId: string, message: SocketMessage) => {
const connections = userConnections.get(userId);
connections?.forEach((connection) => {
@ -61,15 +61,15 @@ const sendMessageToUser = (userId: string, message: SocketMessage) => {
});
};
const sendMessageToUsers = (userIds: string[], message: SocketMessage) => {
const sendMessageToUsersWS = (userIds: string[], message: SocketMessage) => {
userIds?.forEach((userId) => {
sendMessageToUser(userId, message);
sendMessageToUserWS(userId, message);
});
};
export {
userConnections,
handleNewWebSocket,
sendMessageToUser,
sendMessageToUsers,
sendMessageToUserWS,
sendMessageToUsersWS,
};