Add messaging
This commit is contained in:
parent
23128f25e1
commit
5733975aa0
29 changed files with 986 additions and 8 deletions
138
package-lock.json
generated
138
package-lock.json
generated
|
|
@ -1,28 +1,31 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.3.7",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.3.7",
|
"version": "0.4.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
|
"@fastify/websocket": "^11.2.0",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"prisma": "^7.2.0",
|
"prisma": "^7.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|
@ -266,6 +269,27 @@
|
||||||
"ipaddr.js": "^2.1.0"
|
"ipaddr.js": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/websocket": {
|
||||||
|
"version": "11.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.2.0.tgz",
|
||||||
|
"integrity": "sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"duplexify": "^4.1.3",
|
||||||
|
"fastify-plugin": "^5.0.0",
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@hono/node-server": {
|
"node_modules/@hono/node-server": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz",
|
||||||
|
|
@ -606,6 +630,16 @@
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/abstract-logging": {
|
"node_modules/abstract-logging": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||||
|
|
@ -954,6 +988,18 @@
|
||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/duplexify": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1",
|
||||||
|
"stream-shift": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ecdsa-sig-formatter": {
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
|
@ -984,6 +1030,15 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
|
|
@ -1247,6 +1302,12 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
|
||||||
|
|
@ -1576,6 +1637,15 @@
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-key": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
|
|
@ -1927,6 +1997,20 @@
|
||||||
"react": "^19.2.3"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
|
@ -2183,6 +2267,21 @@
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stream-shift": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thread-stream": {
|
"node_modules/thread-stream": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||||
|
|
@ -2289,6 +2388,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "13.0.0",
|
"version": "13.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||||
|
|
@ -2339,6 +2444,33 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.3.7",
|
"version": "0.4.0",
|
||||||
"description": "Communication server using the Nexlink protocol",
|
"description": "Communication server using the Nexlink protocol",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"prisma": "^7.2.0",
|
"prisma": "^7.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|
@ -27,12 +28,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
|
"@fastify/websocket": "^11.2.0",
|
||||||
"@prisma/adapter-pg": "^7.2.0",
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^7.2.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
prisma/migrations/20260111121740_message/migration.sql
Normal file
21
prisma/migrations/20260111121740_message/migration.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Message" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"editHistory" TEXT[],
|
||||||
|
"edited" BOOLEAN NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"channelId" TEXT NOT NULL,
|
||||||
|
"creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Message_id_key" ON "Message"("id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
3
prisma/migrations/20260111124811_message_1/migration.sql
Normal file
3
prisma/migrations/20260111124811_message_1/migration.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Message" ALTER COLUMN "editHistory" SET DEFAULT ARRAY[]::TEXT[],
|
||||||
|
ALTER COLUMN "edited" SET DEFAULT false;
|
||||||
16
prisma/migrations/20260111125054_message_2/migration.sql
Normal file
16
prisma/migrations/20260111125054_message_2/migration.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `userId` on the `Message` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `ownerId` to the `Message` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Message" DROP CONSTRAINT "Message_userId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Message" DROP COLUMN "userId",
|
||||||
|
ADD COLUMN "ownerId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -28,6 +28,7 @@ model Channel {
|
||||||
community Community @relation(fields: [communityId], references: [id], onDelete: Cascade)
|
community Community @relation(fields: [communityId], references: [id], onDelete: Cascade)
|
||||||
communityId String
|
communityId String
|
||||||
creationDate DateTime @default(now())
|
creationDate DateTime @default(now())
|
||||||
|
messages Message[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
|
|
@ -55,6 +56,7 @@ model User {
|
||||||
ownedCommunities Community[] @relation(name: "OwnerCommunityToUser")
|
ownedCommunities Community[] @relation(name: "OwnerCommunityToUser")
|
||||||
communities Community[] @relation(name: "MembersCommunitiesToUsers")
|
communities Community[] @relation(name: "MembersCommunitiesToUsers")
|
||||||
roles Role[] @relation(name: "UsersRolesToUsers")
|
roles Role[] @relation(name: "UsersRolesToUsers")
|
||||||
|
messages Message[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
|
|
@ -76,3 +78,15 @@ model Invite {
|
||||||
creationDate DateTime @default(now())
|
creationDate DateTime @default(now())
|
||||||
expirationDate DateTime?
|
expirationDate DateTime?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Message {
|
||||||
|
id String @id @unique @default(uuid())
|
||||||
|
text String
|
||||||
|
editHistory String[] @default([])
|
||||||
|
edited Boolean @default(false)
|
||||||
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
|
ownerId String
|
||||||
|
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||||
|
channelId String
|
||||||
|
creationDate DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,16 @@ import type {
|
||||||
IDeleteChannelParams,
|
IDeleteChannelParams,
|
||||||
IDeleteChannelResponseError,
|
IDeleteChannelResponseError,
|
||||||
IDeleteChannelResponseSuccess,
|
IDeleteChannelResponseSuccess,
|
||||||
|
IGetMessagesParams,
|
||||||
|
IGetMessagesResponseError,
|
||||||
|
IGetMessagesResponseSuccess,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import {
|
import {
|
||||||
createChannelAuth,
|
createChannelAuth,
|
||||||
deleteChannelByIdAuth,
|
deleteChannelByIdAuth,
|
||||||
getChannelByIdAuth,
|
getChannelByIdAuth,
|
||||||
updateChannelByIdAuth,
|
updateChannelByIdAuth,
|
||||||
|
getChannelMessagesByIdAuth,
|
||||||
} from "../../services/channel/channel.js";
|
} from "../../services/channel/channel.js";
|
||||||
import { API_ERROR } from "../errors.js";
|
import { API_ERROR } from "../errors.js";
|
||||||
|
|
||||||
|
|
@ -135,4 +139,42 @@ const deleteChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IDeleteChannelResponseSuccess;
|
} as IDeleteChannelResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getChannel, postCreateChannel, patchChannel, deleteChannel };
|
const getMessages = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { id } = request.params as IGetMessagesParams;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const messages = await getChannelMessagesByIdAuth(id, authHeader);
|
||||||
|
if (!messages) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetMessagesResponseError;
|
||||||
|
}
|
||||||
|
if (messages === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IGetMessagesResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
messages: messages.map((message) => ({
|
||||||
|
id: message.id,
|
||||||
|
text: message.text,
|
||||||
|
edited: message.edited,
|
||||||
|
ownerId: message.ownerId,
|
||||||
|
creationDate: message.creationDate.getTime(),
|
||||||
|
})),
|
||||||
|
} as IGetMessagesResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getChannel,
|
||||||
|
postCreateChannel,
|
||||||
|
patchChannel,
|
||||||
|
deleteChannel,
|
||||||
|
getMessages,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const channelRoutes = async (fastify: FastifyInstance) => {
|
||||||
fastify.post(`/`, controller.postCreateChannel);
|
fastify.post(`/`, controller.postCreateChannel);
|
||||||
fastify.patch(`/:id`, controller.patchChannel);
|
fastify.patch(`/:id`, controller.patchChannel);
|
||||||
fastify.delete(`/:id`, controller.deleteChannel);
|
fastify.delete(`/:id`, controller.deleteChannel);
|
||||||
|
fastify.get(`/:id/messages`, controller.getMessages);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { channelRoutes };
|
export { channelRoutes };
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,28 @@ interface IDeleteChannelResponseSuccess {
|
||||||
communityId: string;
|
communityId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IGetMessagesParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetMessagesResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetMessagesResponseSuccess {
|
||||||
|
id: string;
|
||||||
|
messages: IGetMessagesResponseMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetMessagesResponseMessage {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
edited: boolean;
|
||||||
|
ownerId: string;
|
||||||
|
creationDate: number;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type IChannel,
|
type IChannel,
|
||||||
type IGetChannelParams,
|
type IGetChannelParams,
|
||||||
|
|
@ -77,4 +99,8 @@ export {
|
||||||
type IDeleteChannelParams,
|
type IDeleteChannelParams,
|
||||||
type IDeleteChannelResponseError,
|
type IDeleteChannelResponseError,
|
||||||
type IDeleteChannelResponseSuccess,
|
type IDeleteChannelResponseSuccess,
|
||||||
|
type IGetMessagesParams,
|
||||||
|
type IGetMessagesResponseError,
|
||||||
|
type IGetMessagesResponseSuccess,
|
||||||
|
type IGetMessagesResponseMessage,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
3
src/controllers/message/index.ts
Normal file
3
src/controllers/message/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./message.js";
|
||||||
|
export * from "./routes.js";
|
||||||
|
export * from "./types.js";
|
||||||
145
src/controllers/message/message.ts
Normal file
145
src/controllers/message/message.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||||
|
import type {
|
||||||
|
IGetMessageParams,
|
||||||
|
IGetMessageResponseError,
|
||||||
|
IGetMessageResponseSuccess,
|
||||||
|
IPostCreateMessageRequest,
|
||||||
|
IPostCreateMessageResponseError,
|
||||||
|
IPostCreateMessageResponseSuccess,
|
||||||
|
IPatchMessageParams,
|
||||||
|
IPatchMessageRequest,
|
||||||
|
IPatchMessageResponseError,
|
||||||
|
IPatchMessageResponseSuccess,
|
||||||
|
IDeleteMessageParams,
|
||||||
|
IDeleteMessageResponseError,
|
||||||
|
IDeleteMessageResponseSuccess,
|
||||||
|
} from "./types.js";
|
||||||
|
import {
|
||||||
|
createMessageAuth,
|
||||||
|
deleteMessageByIdAuth,
|
||||||
|
getMessageByIdAuth,
|
||||||
|
updateMessageByIdAuth,
|
||||||
|
} from "../../services/message/message.js";
|
||||||
|
import { API_ERROR } from "../errors.js";
|
||||||
|
|
||||||
|
const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { id } = request.params as IGetMessageParams;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const message = await getMessageByIdAuth(id, authHeader);
|
||||||
|
if (!message) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetMessageResponseError;
|
||||||
|
}
|
||||||
|
if (message === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IGetMessageResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
text: message.text,
|
||||||
|
editHistory: message.editHistory,
|
||||||
|
edited: message.edited,
|
||||||
|
ownerId: message.ownerId,
|
||||||
|
channelId: message.channelId,
|
||||||
|
creationDate: message.creationDate.getTime(),
|
||||||
|
} as IGetMessageResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const postCreateMessage = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const createMessageRequest = request.body as IPostCreateMessageRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const message = await createMessageAuth(createMessageRequest, authHeader);
|
||||||
|
if (message === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IPostCreateMessageResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
text: message.text,
|
||||||
|
editHistory: message.editHistory,
|
||||||
|
edited: message.edited,
|
||||||
|
ownerId: message.ownerId,
|
||||||
|
channelId: message.channelId,
|
||||||
|
creationDate: message.creationDate.getTime(),
|
||||||
|
} as IPostCreateMessageResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { id } = request.params as IPatchMessageParams;
|
||||||
|
const patchMessageRequest = request.body as IPatchMessageRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const message = await updateMessageByIdAuth(
|
||||||
|
id,
|
||||||
|
patchMessageRequest,
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
if (!message) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IPatchMessageResponseError;
|
||||||
|
}
|
||||||
|
if (message === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IPatchMessageResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
text: message.text,
|
||||||
|
editHistory: message.editHistory,
|
||||||
|
edited: message.edited,
|
||||||
|
ownerId: message.ownerId,
|
||||||
|
channelId: message.channelId,
|
||||||
|
creationDate: message.creationDate.getTime(),
|
||||||
|
} as IPatchMessageResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { id } = request.params as IDeleteMessageParams;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const message = await deleteMessageByIdAuth(id, authHeader);
|
||||||
|
if (!message) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IDeleteMessageResponseError;
|
||||||
|
}
|
||||||
|
if (message === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IDeleteMessageResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
ownerId: message.ownerId,
|
||||||
|
channelId: message.channelId,
|
||||||
|
} as IDeleteMessageResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getMessage, postCreateMessage, patchMessage, deleteMessage };
|
||||||
11
src/controllers/message/routes.ts
Normal file
11
src/controllers/message/routes.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { type FastifyInstance } from "fastify";
|
||||||
|
import * as controller from "./message.js";
|
||||||
|
|
||||||
|
const messageRoutes = async (fastify: FastifyInstance) => {
|
||||||
|
fastify.get(`/:id`, controller.getMessage);
|
||||||
|
fastify.post(`/`, controller.postCreateMessage);
|
||||||
|
fastify.patch(`/:id`, controller.patchMessage);
|
||||||
|
fastify.delete(`/:id`, controller.deleteMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { messageRoutes };
|
||||||
81
src/controllers/message/types.ts
Normal file
81
src/controllers/message/types.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import type { API_ERROR } from "../errors.js";
|
||||||
|
|
||||||
|
interface IMessage {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
editHistory: string[];
|
||||||
|
edited: boolean;
|
||||||
|
ownerId: string;
|
||||||
|
channelId: string;
|
||||||
|
creationDate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetMessageParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetMessageResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetMessageResponseSuccess extends IMessage {}
|
||||||
|
|
||||||
|
interface IPostCreateMessageRequest {
|
||||||
|
text: string;
|
||||||
|
channelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostCreateMessageResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostCreateMessageResponseSuccess extends IMessage {}
|
||||||
|
|
||||||
|
interface IPatchMessageParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPatchMessageRequest {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPatchMessageResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPatchMessageResponseSuccess extends IMessage {}
|
||||||
|
|
||||||
|
interface IDeleteMessageParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDeleteMessageResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDeleteMessageResponseSuccess {
|
||||||
|
id: string;
|
||||||
|
ownerId: string;
|
||||||
|
channelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
type IMessage,
|
||||||
|
type IGetMessageParams,
|
||||||
|
type IGetMessageResponseError,
|
||||||
|
type IGetMessageResponseSuccess,
|
||||||
|
type IPostCreateMessageRequest,
|
||||||
|
type IPostCreateMessageResponseError,
|
||||||
|
type IPostCreateMessageResponseSuccess,
|
||||||
|
type IPatchMessageParams,
|
||||||
|
type IPatchMessageRequest,
|
||||||
|
type IPatchMessageResponseError,
|
||||||
|
type IPatchMessageResponseSuccess,
|
||||||
|
type IDeleteMessageParams,
|
||||||
|
type IDeleteMessageResponseError,
|
||||||
|
type IDeleteMessageResponseSuccess,
|
||||||
|
};
|
||||||
3
src/controllers/websocket/index.ts
Normal file
3
src/controllers/websocket/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./websocket.js";
|
||||||
|
export * from "./routes.js";
|
||||||
|
export * from "./types.js";
|
||||||
12
src/controllers/websocket/routes.ts
Normal file
12
src/controllers/websocket/routes.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { type FastifyInstance } from "fastify";
|
||||||
|
import * as controller from "./websocket.js";
|
||||||
|
|
||||||
|
const websocketRoutes = async (fastify: FastifyInstance) => {
|
||||||
|
fastify.get(
|
||||||
|
`/`,
|
||||||
|
{ websocket: true, preHandler: controller.handleWebSockets },
|
||||||
|
controller.getWebSockets,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { websocketRoutes };
|
||||||
7
src/controllers/websocket/types.ts
Normal file
7
src/controllers/websocket/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { API_ERROR } from "../errors.js";
|
||||||
|
|
||||||
|
interface IGetWebSocketResponseError {
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { type IGetWebSocketResponseError };
|
||||||
33
src/controllers/websocket/websocket.ts
Normal file
33
src/controllers/websocket/websocket.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||||
|
import type { IGetWebSocketResponseError } from "./types.js";
|
||||||
|
import { API_ERROR } from "../errors.js";
|
||||||
|
import { getUserFromCookie } from "../../services/auth/helpers.js";
|
||||||
|
import { handleNewWebSocket } from "../../services/websocket/websocket.js";
|
||||||
|
import type { WebSocket } from "@fastify/websocket";
|
||||||
|
|
||||||
|
const handleWebSockets = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const cookie = request.cookies["token"];
|
||||||
|
const user = await getUserFromCookie(cookie);
|
||||||
|
if (!user) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IGetWebSocketResponseError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWebSockets = async (socket: WebSocket, request: FastifyRequest) => {
|
||||||
|
const cookie = request.cookies["token"];
|
||||||
|
const user = await getUserFromCookie(cookie);
|
||||||
|
if (!user) {
|
||||||
|
socket.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewWebSocket(socket, user.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { handleWebSockets, getWebSockets };
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import cors from "@fastify/cors";
|
import cors from "@fastify/cors";
|
||||||
import cookie from "@fastify/cookie";
|
import cookie from "@fastify/cookie";
|
||||||
|
import websocket from "@fastify/websocket";
|
||||||
|
|
||||||
import { config } from "./config.js";
|
import { config } from "./config.js";
|
||||||
|
|
||||||
|
|
@ -14,6 +15,8 @@ import { communityRoutes } from "./controllers/community/routes.js";
|
||||||
import { channelRoutes } from "./controllers/channel/routes.js";
|
import { channelRoutes } from "./controllers/channel/routes.js";
|
||||||
import { roleRoutes } from "./controllers/role/routes.js";
|
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 { websocketRoutes } from "./controllers/websocket/routes.js";
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
|
|
@ -26,6 +29,8 @@ app.register(cors, {
|
||||||
|
|
||||||
app.register(cookie, { secret: getCookieSecret() });
|
app.register(cookie, { secret: getCookieSecret() });
|
||||||
|
|
||||||
|
app.register(websocket);
|
||||||
|
|
||||||
app.register(testRoutes);
|
app.register(testRoutes);
|
||||||
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
||||||
app.register(userRoutes, { prefix: "/api/v1/user" });
|
app.register(userRoutes, { prefix: "/api/v1/user" });
|
||||||
|
|
@ -34,6 +39,8 @@ app.register(communityRoutes, { prefix: "/api/v1/community" });
|
||||||
app.register(channelRoutes, { prefix: "/api/v1/channel" });
|
app.register(channelRoutes, { prefix: "/api/v1/channel" });
|
||||||
app.register(roleRoutes, { prefix: "/api/v1/role" });
|
app.register(roleRoutes, { prefix: "/api/v1/role" });
|
||||||
app.register(inviteRoutes, { prefix: "/api/v1/invite" });
|
app.register(inviteRoutes, { prefix: "/api/v1/invite" });
|
||||||
|
app.register(messageRoutes, { prefix: "/api/v1/message" });
|
||||||
|
app.register(websocketRoutes, { prefix: "/ws" });
|
||||||
|
|
||||||
app.listen({ port: config.port }, (err, address) => {
|
app.listen({ port: config.port }, (err, address) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,31 @@ const verifyPassword = async (
|
||||||
return await argon2.verify(passwordHash, passwordToCheck);
|
return await argon2.verify(passwordHash, passwordToCheck);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUserFromCookie = async (
|
||||||
|
cookie: string | undefined,
|
||||||
|
): Promise<User | null> => {
|
||||||
|
if (!cookie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getDB().session.findFirst({
|
||||||
|
where: {
|
||||||
|
cookie: cookie,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUserBySessionId(session.id);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
const getUserBySessionId = async (sessionId: string): Promise<User | null> => {
|
const getUserBySessionId = async (sessionId: string): Promise<User | null> => {
|
||||||
return await getDB().user.findFirst({
|
return await getDB().user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -116,6 +141,12 @@ const isUserOwnerOrAdmin = async (
|
||||||
if (ownerCheck.role !== undefined) {
|
if (ownerCheck.role !== undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
ownerCheck.message !== undefined &&
|
||||||
|
ownerCheck.message?.ownerId !== user.id
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
@ -214,6 +245,7 @@ export {
|
||||||
verifyToken,
|
verifyToken,
|
||||||
hashPassword,
|
hashPassword,
|
||||||
verifyPassword,
|
verifyPassword,
|
||||||
|
getUserFromCookie,
|
||||||
getUserBySessionId,
|
getUserBySessionId,
|
||||||
getUserFromAuth,
|
getUserFromAuth,
|
||||||
isUserOwnerOrAdmin,
|
isUserOwnerOrAdmin,
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,12 @@ enum PERMISSION {
|
||||||
MEMBERS_READ = "MEMBERS_READ",
|
MEMBERS_READ = "MEMBERS_READ",
|
||||||
MEMBERS_KICK = "MEMBERS_KICK",
|
MEMBERS_KICK = "MEMBERS_KICK",
|
||||||
MEMBERS_BAN = "MEMBERS_BAN",
|
MEMBERS_BAN = "MEMBERS_BAN",
|
||||||
|
INVITES_READ = "INVITES_READ",
|
||||||
INVITES_CREATE = "INVITES_CREATE",
|
INVITES_CREATE = "INVITES_CREATE",
|
||||||
INVITES_DELETE = "INVITES_DELETE",
|
INVITES_DELETE = "INVITES_DELETE",
|
||||||
|
MESSAGES_READ = "MESSAGES_READ",
|
||||||
|
MESSAGES_CREATE = "MESSAGES_CREATE",
|
||||||
|
MESSAGES_DELETE = "MESSAGES_DELETE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PERMISSION };
|
export { PERMISSION };
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
Role,
|
Role,
|
||||||
Session,
|
Session,
|
||||||
User,
|
User,
|
||||||
|
Message,
|
||||||
} from "../../generated/prisma/client.js";
|
} from "../../generated/prisma/client.js";
|
||||||
|
|
||||||
interface AccessTokenPayload extends JwtPayload {
|
interface AccessTokenPayload extends JwtPayload {
|
||||||
|
|
@ -30,6 +31,7 @@ interface IOwnerCheck {
|
||||||
invite?: Invite | null;
|
invite?: Invite | null;
|
||||||
channel?: Channel | null;
|
channel?: Channel | null;
|
||||||
role?: Role | null;
|
role?: Role | null;
|
||||||
|
message?: Message | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { API_ERROR } from "../../controllers/errors.js";
|
import { API_ERROR } from "../../controllers/errors.js";
|
||||||
import type { Channel } from "../../generated/prisma/client.js";
|
import type { Channel, Message } from "../../generated/prisma/client.js";
|
||||||
import { getDB } from "../../store/store.js";
|
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";
|
||||||
|
|
@ -136,6 +136,40 @@ const deleteChannelByIdAuth = async (
|
||||||
return await deleteChannelById(id);
|
return await deleteChannelById(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getChannelMessagesById = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<Message[] | null> => {
|
||||||
|
return await getDB().message.findMany({
|
||||||
|
where: {
|
||||||
|
channelId: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChannelMessagesByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Message[] | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const channel = await getChannelById(id);
|
||||||
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_READ],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getChannelMessagesById(id);
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getChannelById,
|
getChannelById,
|
||||||
getChannelByIdAuth,
|
getChannelByIdAuth,
|
||||||
|
|
@ -145,4 +179,6 @@ export {
|
||||||
updateChannelByIdAuth,
|
updateChannelByIdAuth,
|
||||||
deleteChannelById,
|
deleteChannelById,
|
||||||
deleteChannelByIdAuth,
|
deleteChannelByIdAuth,
|
||||||
|
getChannelMessagesById,
|
||||||
|
getChannelMessagesByIdAuth,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ const getCommunityInvitesByIdAuth = async (
|
||||||
community: community,
|
community: community,
|
||||||
},
|
},
|
||||||
community,
|
community,
|
||||||
[PERMISSION.INVITES_CREATE],
|
[PERMISSION.INVITES_READ],
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
return API_ERROR.ACCESS_DENIED;
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
|
|
||||||
2
src/services/message/index.ts
Normal file
2
src/services/message/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./message.js";
|
||||||
|
export * from "./types.js";
|
||||||
202
src/services/message/message.ts
Normal file
202
src/services/message/message.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import { API_ERROR } from "../../controllers/errors.js";
|
||||||
|
import type { Message } from "../../generated/prisma/client.js";
|
||||||
|
import { getDB } from "../../store/store.js";
|
||||||
|
import {
|
||||||
|
getUserFromAuth,
|
||||||
|
isUserAllowed,
|
||||||
|
isUserInCommunity,
|
||||||
|
isUserOwnerOrAdmin,
|
||||||
|
} from "../auth/helpers.js";
|
||||||
|
import { PERMISSION } from "../auth/permission.js";
|
||||||
|
import { getChannelById } from "../channel/channel.js";
|
||||||
|
import { getCommunityById } from "../community/community.js";
|
||||||
|
import { SocketMessageTypes } from "../websocket/types.js";
|
||||||
|
import { sendMessageToUsers } from "../websocket/websocket.js";
|
||||||
|
import type { ICreateMessage, IUpdateMessage } from "./types.js";
|
||||||
|
|
||||||
|
const getMessageById = async (id: string): Promise<Message | null> => {
|
||||||
|
return await getDB().message.findUnique({
|
||||||
|
where: { id: id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const message = await getMessageById(id);
|
||||||
|
const channel = await getChannelById(message?.channelId ?? "");
|
||||||
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_READ],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMessage = async (
|
||||||
|
ownerId: string,
|
||||||
|
communityId: string,
|
||||||
|
create: ICreateMessage,
|
||||||
|
): Promise<Message> => {
|
||||||
|
const message = await getDB().message.create({
|
||||||
|
data: {
|
||||||
|
ownerId: ownerId,
|
||||||
|
...create,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const usersInCommunity = await getDB().user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
communities: {
|
||||||
|
some: {
|
||||||
|
id: communityId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const userIds = usersInCommunity.map((user) => user.id);
|
||||||
|
|
||||||
|
sendMessageToUsers(userIds, {
|
||||||
|
type: SocketMessageTypes.NEW_MESSAGE,
|
||||||
|
payload: {
|
||||||
|
channelId: message.channelId,
|
||||||
|
message: {
|
||||||
|
id: message.id,
|
||||||
|
text: message.text,
|
||||||
|
edited: message.edited,
|
||||||
|
ownerId: message.ownerId,
|
||||||
|
creationDate: message.creationDate.getTime(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMessageAuth = async (
|
||||||
|
create: ICreateMessage,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Message | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const channel = await getChannelById(create.channelId);
|
||||||
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!authUser ||
|
||||||
|
!community ||
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_CREATE],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createMessage(authUser.id, community.id, create);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMessageById = async (
|
||||||
|
id: string,
|
||||||
|
update: IUpdateMessage,
|
||||||
|
): Promise<Message | null> => {
|
||||||
|
const message = await getMessageById(id);
|
||||||
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEditHistory = [...message.editHistory, message.text];
|
||||||
|
|
||||||
|
return await getDB().message.update({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...update,
|
||||||
|
editHistory: newEditHistory,
|
||||||
|
edited: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMessageByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
update: IUpdateMessage,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const message = await getMessageById(id);
|
||||||
|
const channel = await getChannelById(message?.channelId ?? "");
|
||||||
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserOwnerOrAdmin(authUser, {
|
||||||
|
message: message,
|
||||||
|
})) ||
|
||||||
|
!(await isUserInCommunity(authUser, community))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await updateMessageById(id, update);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMessageById = async (id: string): Promise<Message | null> => {
|
||||||
|
return await getDB().message.delete({
|
||||||
|
where: { id: id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMessageByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const message = await getMessageById(id);
|
||||||
|
const channel = await getChannelById(message?.channelId ?? "");
|
||||||
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_DELETE],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await deleteMessageById(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getMessageById,
|
||||||
|
getMessageByIdAuth,
|
||||||
|
createMessage,
|
||||||
|
createMessageAuth,
|
||||||
|
updateMessageById,
|
||||||
|
updateMessageByIdAuth,
|
||||||
|
deleteMessageById,
|
||||||
|
deleteMessageByIdAuth,
|
||||||
|
};
|
||||||
10
src/services/message/types.ts
Normal file
10
src/services/message/types.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
interface ICreateMessage {
|
||||||
|
text: string;
|
||||||
|
channelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUpdateMessage {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { type ICreateMessage, type IUpdateMessage };
|
||||||
2
src/services/websocket/index.ts
Normal file
2
src/services/websocket/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./websocket.js";
|
||||||
|
export * from "./types.js";
|
||||||
53
src/services/websocket/types.ts
Normal file
53
src/services/websocket/types.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import type { WebSocket } from "@fastify/websocket";
|
||||||
|
import type { IGetMessagesResponseMessage } from "../../controllers/channel/types.js";
|
||||||
|
|
||||||
|
interface ISocketConnection {
|
||||||
|
socket: WebSocket;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SocketRequestTypes {
|
||||||
|
PING = "PING",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SocketMessageTypes {
|
||||||
|
NEW_ANNOUNCEMENT = "NEW_ANNOUNCEMENT",
|
||||||
|
NEW_MESSAGE = "NEW_MESSAGE",
|
||||||
|
NEW_CHANNEL = "NEW_CHANNEL",
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketRequest = {
|
||||||
|
type: SocketRequestTypes.PING;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SocketMessage =
|
||||||
|
| {
|
||||||
|
type: SocketMessageTypes.NEW_ANNOUNCEMENT;
|
||||||
|
payload: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SocketMessageTypes.NEW_MESSAGE;
|
||||||
|
payload: {
|
||||||
|
channelId: string;
|
||||||
|
message: IGetMessagesResponseMessage;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SocketMessageTypes.NEW_CHANNEL;
|
||||||
|
payload: {
|
||||||
|
id: string;
|
||||||
|
communityId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ISocketConnection,
|
||||||
|
SocketRequestTypes,
|
||||||
|
SocketMessageTypes,
|
||||||
|
type SocketRequest,
|
||||||
|
type SocketMessage,
|
||||||
|
};
|
||||||
75
src/services/websocket/websocket.ts
Normal file
75
src/services/websocket/websocket.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import type { CloseEvent, MessageEvent } from "ws";
|
||||||
|
import type { WebSocket } from "@fastify/websocket";
|
||||||
|
import type {
|
||||||
|
ISocketConnection,
|
||||||
|
SocketRequest,
|
||||||
|
SocketMessage,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
const userConnections = new Map<string, Set<ISocketConnection>>();
|
||||||
|
|
||||||
|
const handleNewWebSocket = (socket: WebSocket, userId: string) => {
|
||||||
|
const connection = {
|
||||||
|
socket: socket,
|
||||||
|
userId: userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!userConnections.has(userId)) {
|
||||||
|
userConnections.set(userId, new Set());
|
||||||
|
}
|
||||||
|
userConnections.get(userId)?.add(connection);
|
||||||
|
|
||||||
|
onCloseWsHandler(connection);
|
||||||
|
onMessageWsHandler(connection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequest = (data: string): SocketRequest | null => {
|
||||||
|
const request = JSON.parse(data) as SocketRequest;
|
||||||
|
|
||||||
|
if (!request.type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCloseWsHandler = (connection: ISocketConnection) => {
|
||||||
|
connection.socket.onclose = (event: CloseEvent) => {
|
||||||
|
const connections = userConnections.get(connection.userId);
|
||||||
|
|
||||||
|
connections?.delete(connection);
|
||||||
|
if (connections?.size === 0) {
|
||||||
|
userConnections.delete(connection.userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessageWsHandler = (connection: ISocketConnection) => {
|
||||||
|
connection.socket.onmessage = (event: MessageEvent) => {
|
||||||
|
const request = handleRequest(event.data.toString());
|
||||||
|
if (!request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessageToUser = (userId: string, message: SocketMessage) => {
|
||||||
|
const connections = userConnections.get(userId);
|
||||||
|
|
||||||
|
connections?.forEach((connection) => {
|
||||||
|
connection.socket.send(JSON.stringify(message));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessageToUsers = (userIds: string[], message: SocketMessage) => {
|
||||||
|
userIds?.forEach((userId) => {
|
||||||
|
sendMessageToUser(userId, message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
userConnections,
|
||||||
|
handleNewWebSocket,
|
||||||
|
sendMessageToUser,
|
||||||
|
sendMessageToUsers,
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue