Added end to end encryption
This commit is contained in:
parent
5733975aa0
commit
6f292756ed
34 changed files with 682 additions and 69 deletions
96
package-lock.json
generated
96
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.4.0",
|
"version": "0.5.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.4.0",
|
"version": "0.5.2",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"ua-parser-js": "^2.0.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
|
|
@ -965,6 +966,26 @@
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-europe-js": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
|
@ -1324,6 +1345,26 @@
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-standalone-pwa": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
|
@ -2381,6 +2422,57 @@
|
||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ua-is-frozen": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ua-parser-js": {
|
||||||
|
"version": "2.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.8.tgz",
|
||||||
|
"integrity": "sha512-BdnBM5waFormdrOFBU+cA90R689V0tWUWlIG2i30UXxElHjuCu5+dOV2Etw3547jcQ/yaLtPm9wrqIuOY2bSJg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-europe-js": "^0.1.2",
|
||||||
|
"is-standalone-pwa": "^0.1.1",
|
||||||
|
"ua-is-frozen": "^0.1.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ua-parser-js": "script/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.4.0",
|
"version": "0.5.2",
|
||||||
"description": "Communication server using the Nexlink protocol",
|
"description": "Communication server using the Nexlink protocol",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
"ua-parser-js": "^2.0.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Session" ADD COLUMN "name" TEXT,
|
||||||
|
ADD COLUMN "sessionStorageKey" TEXT,
|
||||||
|
ADD COLUMN "userAgent" TEXT;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `sessionStorageKey` on the `Session` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Session" DROP COLUMN "sessionStorageKey",
|
||||||
|
ADD COLUMN "storageKey" TEXT;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `storageKey` on the `Session` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Session" DROP COLUMN "storageKey",
|
||||||
|
ADD COLUMN "storageSecret" TEXT;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Session" ADD COLUMN "refreshDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Message" ADD COLUMN "iv" TEXT;
|
||||||
|
|
@ -60,11 +60,15 @@ model User {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
name String?
|
||||||
userId String
|
userAgent String?
|
||||||
cookie String
|
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
creationDate DateTime @default(now())
|
userId String
|
||||||
|
cookie String
|
||||||
|
storageSecret String?
|
||||||
|
creationDate DateTime @default(now())
|
||||||
|
refreshDate DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Invite {
|
model Invite {
|
||||||
|
|
@ -82,6 +86,7 @@ model Invite {
|
||||||
model Message {
|
model Message {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
text String
|
text String
|
||||||
|
iv String?
|
||||||
editHistory String[] @default([])
|
editHistory String[] @default([])
|
||||||
edited Boolean @default(false)
|
edited Boolean @default(false)
|
||||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,12 @@ const postRegister = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
||||||
const postLogin = async (request: FastifyRequest, reply: FastifyReply) => {
|
const postLogin = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { username, password } = request.body as IPostLoginRequest;
|
const { username, password } = request.body as IPostLoginRequest;
|
||||||
|
const userAgent = request.headers["user-agent"];
|
||||||
|
|
||||||
const session = await loginUser({
|
const session = await loginUser({
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
|
userAgent: userAgent ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|
@ -85,6 +87,7 @@ const getRefresh = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
id: refresh[0].id,
|
id: refresh[0].id,
|
||||||
ownerId: refresh[0].userId,
|
ownerId: refresh[0].userId,
|
||||||
token: refresh[1],
|
token: refresh[1],
|
||||||
|
storageSecret: refresh[0].storageSecret,
|
||||||
} as IGetRefreshResponseSuccess;
|
} as IGetRefreshResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ interface IGetRefreshResponseSuccess {
|
||||||
id: string;
|
id: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
token: string;
|
token: string;
|
||||||
|
storageSecret: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGetRefreshResponseError {
|
interface IGetRefreshResponseError {
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ const getMessages = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
messages: messages.map((message) => ({
|
messages: messages.map((message) => ({
|
||||||
id: message.id,
|
id: message.id,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
|
iv: message.iv,
|
||||||
edited: message.edited,
|
edited: message.edited,
|
||||||
ownerId: message.ownerId,
|
ownerId: message.ownerId,
|
||||||
creationDate: message.creationDate.getTime(),
|
creationDate: message.creationDate.getTime(),
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ interface IGetMessagesResponseSuccess {
|
||||||
interface IGetMessagesResponseMessage {
|
interface IGetMessagesResponseMessage {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
iv: string;
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,21 @@ import type {
|
||||||
IPostCreateInviteRequest,
|
IPostCreateInviteRequest,
|
||||||
IPostCreateInviteResponseError,
|
IPostCreateInviteResponseError,
|
||||||
IPostCreateInviteResponseSuccess,
|
IPostCreateInviteResponseSuccess,
|
||||||
|
IDeleteMemberParams,
|
||||||
|
IDeleteMemberResponseError,
|
||||||
|
IDeleteMemberResponseSuccess,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import {
|
import {
|
||||||
getCommunityById,
|
getCommunityById,
|
||||||
createCommunityAuth,
|
createCommunityAuth,
|
||||||
updateCommunityByIdAuth,
|
updateCommunityByIdAuth,
|
||||||
|
deleteCommunityByIdAuth,
|
||||||
getCommunityChannelsByIdAuth,
|
getCommunityChannelsByIdAuth,
|
||||||
getCommunityMembersByIdAuth,
|
getCommunityMembersByIdAuth,
|
||||||
getCommunityRolesByIdAuth,
|
getCommunityRolesByIdAuth,
|
||||||
getCommunityInvitesByIdAuth,
|
getCommunityInvitesByIdAuth,
|
||||||
createInviteAuth,
|
createInviteAuth,
|
||||||
deleteCommunityByIdAuth,
|
deleteMemberByIdAuth,
|
||||||
} from "../../services/community/community.js";
|
} from "../../services/community/community.js";
|
||||||
import { API_ERROR } from "../errors.js";
|
import { API_ERROR } from "../errors.js";
|
||||||
import type { ICreateInvite } from "../../services/community/types.js";
|
import type { ICreateInvite } from "../../services/community/types.js";
|
||||||
|
|
@ -318,6 +322,35 @@ const postCreateInvite = async (
|
||||||
} as IPostCreateInviteResponseSuccess;
|
} 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 {
|
export {
|
||||||
getCommunity,
|
getCommunity,
|
||||||
postCreateCommunity,
|
postCreateCommunity,
|
||||||
|
|
@ -328,4 +361,5 @@ export {
|
||||||
getRoles,
|
getRoles,
|
||||||
getInvites,
|
getInvites,
|
||||||
postCreateInvite,
|
postCreateInvite,
|
||||||
|
deleteCommunityMember,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const communityRoutes = async (fastify: FastifyInstance) => {
|
||||||
fastify.get(`/:id/roles`, controller.getRoles);
|
fastify.get(`/:id/roles`, controller.getRoles);
|
||||||
fastify.get(`/:id/invites`, controller.getInvites);
|
fastify.get(`/:id/invites`, controller.getInvites);
|
||||||
fastify.post(`/:id/invite`, controller.postCreateInvite);
|
fastify.post(`/:id/invite`, controller.postCreateInvite);
|
||||||
|
fastify.delete(`/:id/members/:memberId`, controller.deleteCommunityMember);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { communityRoutes };
|
export { communityRoutes };
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,21 @@ interface IPostCreateInviteResponseSuccess {
|
||||||
inviteId: string;
|
inviteId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IDeleteMemberParams {
|
||||||
|
id: string;
|
||||||
|
memberId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDeleteMemberResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDeleteMemberResponseSuccess {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ICommunity,
|
type ICommunity,
|
||||||
type IGetCommunityParams,
|
type IGetCommunityParams,
|
||||||
|
|
@ -193,4 +208,7 @@ export {
|
||||||
type IPostCreateInviteRequest,
|
type IPostCreateInviteRequest,
|
||||||
type IPostCreateInviteResponseError,
|
type IPostCreateInviteResponseError,
|
||||||
type IPostCreateInviteResponseSuccess,
|
type IPostCreateInviteResponseSuccess,
|
||||||
|
type IDeleteMemberParams,
|
||||||
|
type IDeleteMemberResponseError,
|
||||||
|
type IDeleteMemberResponseSuccess,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
|
iv: message.iv,
|
||||||
editHistory: message.editHistory,
|
editHistory: message.editHistory,
|
||||||
edited: message.edited,
|
edited: message.edited,
|
||||||
ownerId: message.ownerId,
|
ownerId: message.ownerId,
|
||||||
|
|
@ -71,6 +72,7 @@ const postCreateMessage = async (
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
|
iv: message.iv,
|
||||||
editHistory: message.editHistory,
|
editHistory: message.editHistory,
|
||||||
edited: message.edited,
|
edited: message.edited,
|
||||||
ownerId: message.ownerId,
|
ownerId: message.ownerId,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { API_ERROR } from "../errors.js";
|
||||||
interface IMessage {
|
interface IMessage {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
iv: string;
|
||||||
editHistory: string[];
|
editHistory: string[];
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
|
|
@ -23,6 +24,7 @@ interface IGetMessageResponseSuccess extends IMessage {}
|
||||||
|
|
||||||
interface IPostCreateMessageRequest {
|
interface IPostCreateMessageRequest {
|
||||||
text: string;
|
text: string;
|
||||||
|
iv: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,10 @@ const getSession = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
|
name: session.name,
|
||||||
|
userAgent: session.userAgent,
|
||||||
creationDate: session.creationDate.getTime(),
|
creationDate: session.creationDate.getTime(),
|
||||||
|
refreshDate: session.refreshDate.getTime(),
|
||||||
} as IGetSessionResponseSuccess;
|
} as IGetSessionResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ import type { API_ERROR } from "../errors.js";
|
||||||
interface ISession {
|
interface ISession {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
userAgent: string;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
|
refreshDate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGetSessionParams {
|
interface IGetSessionParams {
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,10 @@ interface IGetSessionsResponseSuccess {
|
||||||
interface IGetSessionsResponseSession {
|
interface IGetSessionsResponseSession {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
userAgent: string;
|
||||||
|
creationDate: number;
|
||||||
|
refreshDate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGetCommunitiesParams {
|
interface IGetCommunitiesParams {
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,10 @@ const getSessions = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
sessions: sessions.map((session) => ({
|
sessions: sessions.map((session) => ({
|
||||||
id: session.id,
|
id: session.id,
|
||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
|
name: session.name,
|
||||||
|
userAgent: session.userAgent,
|
||||||
|
creationDate: session.creationDate.getTime(),
|
||||||
|
refreshDate: session.refreshDate.getTime(),
|
||||||
})),
|
})),
|
||||||
} as IGetSessionsResponseSuccess;
|
} as IGetSessionsResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { roleRoutes } from "./controllers/role/routes.js";
|
||||||
import { inviteRoutes } from "./controllers/invite/routes.js";
|
import { inviteRoutes } from "./controllers/invite/routes.js";
|
||||||
import { messageRoutes } from "./controllers/message/routes.js";
|
import { messageRoutes } from "./controllers/message/routes.js";
|
||||||
import { websocketRoutes } from "./controllers/websocket/routes.js";
|
import { websocketRoutes } from "./controllers/websocket/routes.js";
|
||||||
|
import { initializeCommunitiesWithReadableUsersCache } from "./services/user/user.js";
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
|
|
@ -25,6 +26,7 @@ const app = Fastify({
|
||||||
app.register(cors, {
|
app.register(cors, {
|
||||||
origin: "http://localhost:3000",
|
origin: "http://localhost:3000",
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(cookie, { secret: getCookieSecret() });
|
app.register(cookie, { secret: getCookieSecret() });
|
||||||
|
|
@ -46,3 +48,5 @@ app.listen({ port: config.port }, (err, address) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
console.log(`Server is now listening on ${address}`);
|
console.log(`Server is now listening on ${address}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
initializeCommunitiesWithReadableUsersCache();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import { UAParser } from "ua-parser-js";
|
||||||
import type { User, Session } from "../../generated/prisma/client.js";
|
import type { User, Session } from "../../generated/prisma/client.js";
|
||||||
import { getDB } from "../../store/store.js";
|
import { getDB } from "../../store/store.js";
|
||||||
import {
|
import {
|
||||||
createSessionCookie,
|
createSessionCookie,
|
||||||
createToken,
|
createToken,
|
||||||
|
getRandomBytesHex,
|
||||||
hashPassword,
|
hashPassword,
|
||||||
verifyPassword,
|
verifyPassword,
|
||||||
} from "./helpers.js";
|
} from "./helpers.js";
|
||||||
|
|
@ -45,6 +47,9 @@ const registerUser = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginUser = async (login: IUserLogin): Promise<Session | null> => {
|
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({
|
const user = await getDB().user.findUnique({
|
||||||
where: { username: login.username },
|
where: { username: login.username },
|
||||||
});
|
});
|
||||||
|
|
@ -69,6 +74,9 @@ const loginUser = async (login: IUserLogin): Promise<Session | null> => {
|
||||||
data: {
|
data: {
|
||||||
cookie: createSessionCookie(),
|
cookie: createSessionCookie(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
name: sessionName,
|
||||||
|
userAgent: login.userAgent,
|
||||||
|
storageSecret: `${getRandomBytesHex(32)};${getRandomBytesHex(12)}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -90,6 +98,15 @@ const refreshSession = async (
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await getDB().session.update({
|
||||||
|
where: {
|
||||||
|
id: session.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
refreshDate: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return [session, createToken(session.id)];
|
return [session, createToken(session.id)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,12 @@ const getCookieSecret = (): string => {
|
||||||
return process.env.COOKIE_SECRET || "";
|
return process.env.COOKIE_SECRET || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRandomBytesHex = (amount: number) => {
|
||||||
|
return crypto.randomBytes(amount).toString("hex");
|
||||||
|
};
|
||||||
|
|
||||||
const createSessionCookie = () => {
|
const createSessionCookie = () => {
|
||||||
return crypto.randomBytes(32).toString("hex");
|
return getRandomBytesHex(32);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createToken = (sessionId: string) => {
|
const createToken = (sessionId: string) => {
|
||||||
|
|
@ -152,19 +156,15 @@ const isUserOwnerOrAdmin = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserPermissions = async (
|
const getUserPermissions = async (
|
||||||
user: User | null,
|
userId: string,
|
||||||
community: Community | null,
|
communityId: string,
|
||||||
): Promise<PERMISSION[]> => {
|
): Promise<PERMISSION[]> => {
|
||||||
if (!user || !community) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const roles = await getDB().role.findMany({
|
const roles = await getDB().role.findMany({
|
||||||
where: {
|
where: {
|
||||||
communityId: community.id,
|
communityId: communityId,
|
||||||
users: {
|
users: {
|
||||||
some: {
|
some: {
|
||||||
id: user.id,
|
id: userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -184,15 +184,15 @@ const getUserPermissions = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const userHasPermissions = async (
|
const userHasPermissions = async (
|
||||||
user: User | null,
|
userId: string | undefined,
|
||||||
community: Community | null,
|
communityId: string | undefined,
|
||||||
requiredPermissions: PERMISSION[],
|
requiredPermissions: PERMISSION[],
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (!user || !community) {
|
if (!userId || !communityId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userPermissions = await getUserPermissions(user, community);
|
const userPermissions = await getUserPermissions(userId, communityId);
|
||||||
|
|
||||||
return requiredPermissions.every((requiredPermission) =>
|
return requiredPermissions.every((requiredPermission) =>
|
||||||
userPermissions.includes(requiredPermission),
|
userPermissions.includes(requiredPermission),
|
||||||
|
|
@ -208,7 +208,9 @@ const isUserAllowed = async (
|
||||||
if (await isUserOwnerOrAdmin(user, ownerCheck)) {
|
if (await isUserOwnerOrAdmin(user, ownerCheck)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (await userHasPermissions(user, community, requiredPermissions)) {
|
if (
|
||||||
|
await userHasPermissions(user?.id, community?.id, requiredPermissions)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,6 +242,7 @@ const isUserInCommunity = async (
|
||||||
export {
|
export {
|
||||||
getJwtSecret,
|
getJwtSecret,
|
||||||
getCookieSecret,
|
getCookieSecret,
|
||||||
|
getRandomBytesHex,
|
||||||
createSessionCookie,
|
createSessionCookie,
|
||||||
createToken,
|
createToken,
|
||||||
verifyToken,
|
verifyToken,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ interface IUserRegistration {
|
||||||
interface IUserLogin {
|
interface IUserLogin {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
userAgent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IOwnerCheck {
|
interface IOwnerCheck {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { getDB } from "../../store/store.js";
|
||||||
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
||||||
import { PERMISSION } from "../auth/permission.js";
|
import { PERMISSION } from "../auth/permission.js";
|
||||||
import { getCommunityById } from "../community/community.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";
|
import type { ICreateChannel, IUpdateChannel } from "./types.js";
|
||||||
|
|
||||||
const getChannelById = async (id: string): Promise<Channel | null> => {
|
const getChannelById = async (id: string): Promise<Channel | null> => {
|
||||||
|
|
@ -37,11 +40,22 @@ const getChannelByIdAuth = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const createChannel = async (create: ICreateChannel): Promise<Channel> => {
|
const createChannel = async (create: ICreateChannel): Promise<Channel> => {
|
||||||
return await getDB().channel.create({
|
const createdChannel = await getDB().channel.create({
|
||||||
data: {
|
data: {
|
||||||
...create,
|
...create,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userIds = await getUserIdsInCommunity(createdChannel.communityId);
|
||||||
|
|
||||||
|
sendMessageToUsersWS(userIds, {
|
||||||
|
type: SocketMessageTypes.UPDATE_CHANNELS,
|
||||||
|
payload: {
|
||||||
|
communityId: createdChannel.communityId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdChannel;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createChannelAuth = async (
|
const createChannelAuth = async (
|
||||||
|
|
@ -71,7 +85,7 @@ const updateChannelById = async (
|
||||||
id: string,
|
id: string,
|
||||||
update: IUpdateChannel,
|
update: IUpdateChannel,
|
||||||
): Promise<Channel | null> => {
|
): Promise<Channel | null> => {
|
||||||
return await getDB().channel.update({
|
const updatedChannel = await getDB().channel.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
|
|
@ -79,6 +93,17 @@ const updateChannelById = async (
|
||||||
...update,
|
...update,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userIds = await getUserIdsInCommunity(updatedChannel.communityId);
|
||||||
|
|
||||||
|
sendMessageToUsersWS(userIds, {
|
||||||
|
type: SocketMessageTypes.UPDATE_CHANNELS,
|
||||||
|
payload: {
|
||||||
|
communityId: updatedChannel.communityId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedChannel;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateChannelByIdAuth = async (
|
const updateChannelByIdAuth = async (
|
||||||
|
|
@ -107,9 +132,20 @@ const updateChannelByIdAuth = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteChannelById = async (id: string): Promise<Channel | null> => {
|
const deleteChannelById = async (id: string): Promise<Channel | null> => {
|
||||||
return await getDB().channel.delete({
|
const deletedChannel = await getDB().channel.delete({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userIds = await getUserIdsInCommunity(deletedChannel.communityId);
|
||||||
|
|
||||||
|
sendMessageToUsersWS(userIds, {
|
||||||
|
type: SocketMessageTypes.UPDATE_CHANNELS,
|
||||||
|
payload: {
|
||||||
|
communityId: deletedChannel.communityId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletedChannel;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteChannelByIdAuth = async (
|
const deleteChannelByIdAuth = async (
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import {
|
||||||
isUserOwnerOrAdmin,
|
isUserOwnerOrAdmin,
|
||||||
} from "../auth/helpers.js";
|
} from "../auth/helpers.js";
|
||||||
import { PERMISSION } from "../auth/permission.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 {
|
import type {
|
||||||
ICreateCommunity,
|
ICreateCommunity,
|
||||||
IUpdateCommunity,
|
IUpdateCommunity,
|
||||||
|
|
@ -287,6 +290,7 @@ const createInviteAuth = async (
|
||||||
const community = await getCommunityById(id);
|
const community = await getCommunityById(id);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!authUser ||
|
||||||
!(await isUserAllowed(
|
!(await isUserAllowed(
|
||||||
authUser,
|
authUser,
|
||||||
{
|
{
|
||||||
|
|
@ -294,8 +298,7 @@ const createInviteAuth = async (
|
||||||
},
|
},
|
||||||
community,
|
community,
|
||||||
[PERMISSION.INVITES_CREATE],
|
[PERMISSION.INVITES_CREATE],
|
||||||
)) ||
|
))
|
||||||
!authUser
|
|
||||||
) {
|
) {
|
||||||
return API_ERROR.ACCESS_DENIED;
|
return API_ERROR.ACCESS_DENIED;
|
||||||
}
|
}
|
||||||
|
|
@ -303,6 +306,60 @@ const createInviteAuth = async (
|
||||||
return await createInvite(id, authUser.id, createInviteData);
|
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 {
|
export {
|
||||||
getCommunityById,
|
getCommunityById,
|
||||||
createCommunity,
|
createCommunity,
|
||||||
|
|
@ -321,4 +378,6 @@ export {
|
||||||
getCommunityInvitesByIdAuth,
|
getCommunityInvitesByIdAuth,
|
||||||
createInvite,
|
createInvite,
|
||||||
createInviteAuth,
|
createInviteAuth,
|
||||||
|
deleteMemberById,
|
||||||
|
deleteMemberByIdAuth,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ import { getDB } from "../../store/store.js";
|
||||||
import { getCommunityById } from "../community/community.js";
|
import { getCommunityById } from "../community/community.js";
|
||||||
import { PERMISSION } from "../auth/permission.js";
|
import { PERMISSION } from "../auth/permission.js";
|
||||||
import { API_ERROR } from "../../controllers/errors.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> => {
|
const getInviteById = async (id: string): Promise<Invite | null> => {
|
||||||
return await getDB().invite.findUnique({
|
return await getDB().invite.findUnique({
|
||||||
|
|
@ -63,7 +66,7 @@ const acceptInviteById = async (
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await getDB().community.update({
|
const updatedCommunity = await getDB().community.update({
|
||||||
where: {
|
where: {
|
||||||
id: invite.communityId,
|
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 (
|
const acceptInviteByIdAuth = async (
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import {
|
||||||
import { PERMISSION } from "../auth/permission.js";
|
import { PERMISSION } from "../auth/permission.js";
|
||||||
import { getChannelById } from "../channel/channel.js";
|
import { getChannelById } from "../channel/channel.js";
|
||||||
import { getCommunityById } from "../community/community.js";
|
import { getCommunityById } from "../community/community.js";
|
||||||
|
import { getUserIdsInCommunityReadMessagesPermission } from "../user/user.js";
|
||||||
import { SocketMessageTypes } from "../websocket/types.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";
|
import type { ICreateMessage, IUpdateMessage } from "./types.js";
|
||||||
|
|
||||||
const getMessageById = async (id: string): Promise<Message | null> => {
|
const getMessageById = async (id: string): Promise<Message | null> => {
|
||||||
|
|
@ -57,27 +58,17 @@ const createMessage = async (
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const usersInCommunity = await getDB().user.findMany({
|
const userIds =
|
||||||
select: {
|
await getUserIdsInCommunityReadMessagesPermission(communityId);
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
communities: {
|
|
||||||
some: {
|
|
||||||
id: communityId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const userIds = usersInCommunity.map((user) => user.id);
|
|
||||||
|
|
||||||
sendMessageToUsers(userIds, {
|
sendMessageToUsersWS(userIds, {
|
||||||
type: SocketMessageTypes.NEW_MESSAGE,
|
type: SocketMessageTypes.SET_MESSAGE,
|
||||||
payload: {
|
payload: {
|
||||||
channelId: message.channelId,
|
channelId: message.channelId,
|
||||||
message: {
|
message: {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
|
iv: message.iv ?? "",
|
||||||
edited: message.edited,
|
edited: message.edited,
|
||||||
ownerId: message.ownerId,
|
ownerId: message.ownerId,
|
||||||
creationDate: message.creationDate.getTime(),
|
creationDate: message.creationDate.getTime(),
|
||||||
|
|
@ -116,6 +107,7 @@ const createMessageAuth = async (
|
||||||
|
|
||||||
const updateMessageById = async (
|
const updateMessageById = async (
|
||||||
id: string,
|
id: string,
|
||||||
|
communityId: string,
|
||||||
update: IUpdateMessage,
|
update: IUpdateMessage,
|
||||||
): Promise<Message | null> => {
|
): Promise<Message | null> => {
|
||||||
const message = await getMessageById(id);
|
const message = await getMessageById(id);
|
||||||
|
|
@ -125,7 +117,7 @@ const updateMessageById = async (
|
||||||
|
|
||||||
const newEditHistory = [...message.editHistory, message.text];
|
const newEditHistory = [...message.editHistory, message.text];
|
||||||
|
|
||||||
return await getDB().message.update({
|
const updatedMessage = await getDB().message.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
|
|
@ -135,6 +127,26 @@ const updateMessageById = async (
|
||||||
edited: true,
|
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 (
|
const updateMessageByIdAuth = async (
|
||||||
|
|
@ -148,6 +160,7 @@ const updateMessageByIdAuth = async (
|
||||||
const community = await getCommunityById(channel?.communityId ?? "");
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!community ||
|
||||||
!(await isUserOwnerOrAdmin(authUser, {
|
!(await isUserOwnerOrAdmin(authUser, {
|
||||||
message: message,
|
message: message,
|
||||||
})) ||
|
})) ||
|
||||||
|
|
@ -156,13 +169,29 @@ const updateMessageByIdAuth = async (
|
||||||
return API_ERROR.ACCESS_DENIED;
|
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> => {
|
const deleteMessageById = async (
|
||||||
return await getDB().message.delete({
|
id: string,
|
||||||
|
communityId: string,
|
||||||
|
): Promise<Message | null> => {
|
||||||
|
const deletedMessage = await getDB().message.delete({
|
||||||
where: { id: id },
|
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 (
|
const deleteMessageByIdAuth = async (
|
||||||
|
|
@ -175,6 +204,7 @@ const deleteMessageByIdAuth = async (
|
||||||
const community = await getCommunityById(channel?.communityId ?? "");
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!community ||
|
||||||
!(await isUserAllowed(
|
!(await isUserAllowed(
|
||||||
authUser,
|
authUser,
|
||||||
{
|
{
|
||||||
|
|
@ -187,7 +217,7 @@ const deleteMessageByIdAuth = async (
|
||||||
return API_ERROR.ACCESS_DENIED;
|
return API_ERROR.ACCESS_DENIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await deleteMessageById(id);
|
return await deleteMessageById(id, community.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
interface ICreateMessage {
|
interface ICreateMessage {
|
||||||
text: string;
|
text: string;
|
||||||
|
iv: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@ import { getDB } from "../../store/store.js";
|
||||||
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
||||||
import { PERMISSION } from "../auth/permission.js";
|
import { PERMISSION } from "../auth/permission.js";
|
||||||
import { getCommunityById } from "../community/community.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";
|
import type { ICreateRole, IUpdateRole } from "./types.js";
|
||||||
|
|
||||||
const getRoleById = async (id: string): Promise<Role | null> => {
|
const getRoleById = async (id: string): Promise<Role | null> => {
|
||||||
|
|
@ -37,11 +44,22 @@ const getRoleByIdAuth = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRole = async (create: ICreateRole): Promise<Role> => {
|
const createRole = async (create: ICreateRole): Promise<Role> => {
|
||||||
return await getDB().role.create({
|
const createdRole = await getDB().role.create({
|
||||||
data: {
|
data: {
|
||||||
...create,
|
...create,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userIds = await getUserIdsInCommunity(createdRole.communityId);
|
||||||
|
|
||||||
|
sendMessageToUsersWS(userIds, {
|
||||||
|
type: SocketMessageTypes.UPDATE_ROLES,
|
||||||
|
payload: {
|
||||||
|
communityId: createdRole.communityId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdRole;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRoleAuth = async (
|
const createRoleAuth = async (
|
||||||
|
|
@ -71,7 +89,7 @@ const updateRoleById = async (
|
||||||
id: string,
|
id: string,
|
||||||
update: IUpdateRole,
|
update: IUpdateRole,
|
||||||
): Promise<Role | null> => {
|
): Promise<Role | null> => {
|
||||||
return await getDB().role.update({
|
const updatedRole = await getDB().role.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
|
|
@ -79,6 +97,30 @@ const updateRoleById = async (
|
||||||
...update,
|
...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 (
|
const updateRoleByIdAuth = async (
|
||||||
|
|
@ -107,9 +149,29 @@ const updateRoleByIdAuth = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRoleById = async (id: string): Promise<Role | null> => {
|
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 },
|
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 (
|
const deleteRoleByIdAuth = async (
|
||||||
|
|
@ -140,7 +202,7 @@ const assignRoleById = async (
|
||||||
id: string,
|
id: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<Role | null> => {
|
): Promise<Role | null> => {
|
||||||
return await getDB().role.update({
|
const updatedRole = await getDB().role.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
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 (
|
const assignRoleByIdAuth = async (
|
||||||
|
|
@ -183,7 +265,7 @@ const unassignRoleById = async (
|
||||||
id: string,
|
id: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<Role | null> => {
|
): Promise<Role | null> => {
|
||||||
return await getDB().role.update({
|
const updatedRole = await getDB().role.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
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 (
|
const unassignRoleByIdAuth = async (
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,82 @@ import type {
|
||||||
Session,
|
Session,
|
||||||
Community,
|
Community,
|
||||||
} from "../../generated/prisma/client.js";
|
} 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 { getDB } from "../../store/store.js";
|
||||||
import { API_ERROR } from "../../controllers/errors.js";
|
import { API_ERROR } from "../../controllers/errors.js";
|
||||||
import type { ICreateUser, IUpdateUser } from "./types.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 (
|
const getLoggedUserAuth = async (
|
||||||
authHeader: string | undefined,
|
authHeader: string | undefined,
|
||||||
|
|
@ -182,7 +254,57 @@ const getUserCommunitiesByIdAuth = async (
|
||||||
return communities;
|
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 {
|
export {
|
||||||
|
communitiesWithReadableUsersCache,
|
||||||
|
initializeCommunitiesWithReadableUsersCache,
|
||||||
|
updateCommunitiesWithReadableUsersCache,
|
||||||
getLoggedUserAuth,
|
getLoggedUserAuth,
|
||||||
getUserById,
|
getUserById,
|
||||||
getUserByIdAuth,
|
getUserByIdAuth,
|
||||||
|
|
@ -196,4 +318,7 @@ export {
|
||||||
getUserSessionsByIdAuth,
|
getUserSessionsByIdAuth,
|
||||||
getUserCommunitiesById,
|
getUserCommunitiesById,
|
||||||
getUserCommunitiesByIdAuth,
|
getUserCommunitiesByIdAuth,
|
||||||
|
getUserIdsInCommunity,
|
||||||
|
getUserIdsInCommunityReadMessagesPermission,
|
||||||
|
getUserIdsWithRole,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,12 @@ enum SocketRequestTypes {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SocketMessageTypes {
|
enum SocketMessageTypes {
|
||||||
NEW_ANNOUNCEMENT = "NEW_ANNOUNCEMENT",
|
ANNOUNCEMENT = "ANNOUNCEMENT",
|
||||||
NEW_MESSAGE = "NEW_MESSAGE",
|
SET_MESSAGE = "SET_MESSAGE",
|
||||||
NEW_CHANNEL = "NEW_CHANNEL",
|
DELETE_MESSAGE = "DELETE_MESSAGE",
|
||||||
|
UPDATE_CHANNELS = "UPDATE_CHANNELS",
|
||||||
|
UPDATE_ROLES = "UPDATE_ROLES",
|
||||||
|
UPDATE_MEMBERS = "UPDATE_MEMBERS",
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocketRequest = {
|
type SocketRequest = {
|
||||||
|
|
@ -22,25 +25,42 @@ type SocketRequest = {
|
||||||
|
|
||||||
type SocketMessage =
|
type SocketMessage =
|
||||||
| {
|
| {
|
||||||
type: SocketMessageTypes.NEW_ANNOUNCEMENT;
|
type: SocketMessageTypes.ANNOUNCEMENT;
|
||||||
payload: {
|
payload: {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: SocketMessageTypes.NEW_MESSAGE;
|
type: SocketMessageTypes.SET_MESSAGE;
|
||||||
payload: {
|
payload: {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
message: IGetMessagesResponseMessage;
|
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: {
|
payload: {
|
||||||
id: string;
|
|
||||||
communityId: string;
|
communityId: string;
|
||||||
name: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
const connections = userConnections.get(userId);
|
||||||
|
|
||||||
connections?.forEach((connection) => {
|
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) => {
|
userIds?.forEach((userId) => {
|
||||||
sendMessageToUser(userId, message);
|
sendMessageToUserWS(userId, message);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
userConnections,
|
userConnections,
|
||||||
handleNewWebSocket,
|
handleNewWebSocket,
|
||||||
sendMessageToUser,
|
sendMessageToUserWS,
|
||||||
sendMessageToUsers,
|
sendMessageToUsersWS,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue