End to end encrypted attachment upload and streaming
This commit is contained in:
parent
6f292756ed
commit
603d969972
63 changed files with 1926 additions and 156 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -132,3 +132,4 @@ dist
|
||||||
|
|
||||||
|
|
||||||
/src/generated/prisma
|
/src/generated/prisma
|
||||||
|
files
|
||||||
|
|
|
||||||
84
package-lock.json
generated
84
package-lock.json
generated
|
|
@ -1,22 +1,24 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.5.2",
|
"version": "0.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.5.2",
|
"version": "0.6.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/multipart": "^9.3.0",
|
||||||
"@fastify/websocket": "^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",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"ua-parser-js": "^2.0.8",
|
"ua-parser-js": "^2.0.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
|
|
@ -24,6 +26,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/mime-types": "^3.0.1",
|
||||||
"@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",
|
"@types/ws": "^8.18.1",
|
||||||
|
|
@ -140,6 +143,12 @@
|
||||||
"fast-uri": "^3.0.0"
|
"fast-uri": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/busboy": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@fastify/cookie": {
|
"node_modules/@fastify/cookie": {
|
||||||
"version": "11.0.2",
|
"version": "11.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz",
|
||||||
|
|
@ -180,6 +189,22 @@
|
||||||
"toad-cache": "^3.7.0"
|
"toad-cache": "^3.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/deepmerge": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@fastify/error": {
|
"node_modules/@fastify/error": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
|
||||||
|
|
@ -250,6 +275,29 @@
|
||||||
"dequal": "^2.0.3"
|
"dequal": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fastify/multipart": {
|
||||||
|
"version": "9.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-9.3.0.tgz",
|
||||||
|
"integrity": "sha512-NpeKipTOjjL1dA7SSlRMrOWWtrE8/0yKOmeudkdQoEaz4sVDJw5MVdZIahsWhvpc3YTN7f04f9ep/Y65RKoOWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/busboy": "^3.0.0",
|
||||||
|
"@fastify/deepmerge": "^3.0.0",
|
||||||
|
"@fastify/error": "^4.0.0",
|
||||||
|
"fastify-plugin": "^5.0.0",
|
||||||
|
"secure-json-parse": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fastify/proxy-addr": {
|
"node_modules/@fastify/proxy-addr": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
|
||||||
|
|
@ -591,6 +639,13 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mime-types": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
|
@ -1575,6 +1630,31 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tether",
|
"name": "tether",
|
||||||
"version": "0.5.2",
|
"version": "0.6.0",
|
||||||
"description": "Communication server using the Nexlink protocol",
|
"description": "Communication server using the Nexlink protocol",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/mime-types": "^3.0.1",
|
||||||
"@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",
|
"@types/ws": "^8.18.1",
|
||||||
|
|
@ -28,12 +29,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
|
"@fastify/multipart": "^9.3.0",
|
||||||
"@fastify/websocket": "^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",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"ua-parser-js": "^2.0.8",
|
"ua-parser-js": "^2.0.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `edited` on the `Message` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Community" ADD COLUMN "avatar" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Message" DROP COLUMN "edited",
|
||||||
|
ADD COLUMN "replyToId" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "avatar" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Reaction" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Reaction_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Attachment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"mimeType" TEXT NOT NULL,
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Attachment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Chunk" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"iv" TEXT NOT NULL,
|
||||||
|
"attachmentId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Chunk_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Reaction_id_key" ON "Reaction"("id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Attachment_id_key" ON "Attachment"("id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Chunk_id_key" ON "Chunk"("id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Chunk" ADD CONSTRAINT "Chunk_attachmentId_fkey" FOREIGN KEY ("attachmentId") REFERENCES "Attachment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `userId` on the `Reaction` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Reaction" DROP COLUMN "userId";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ReactionToUser" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_ReactionToUser_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ReactionToUser_B_index" ON "_ReactionToUser"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ReactionToUser" ADD CONSTRAINT "_ReactionToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Reaction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ReactionToUser" ADD CONSTRAINT "_ReactionToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Message" ADD COLUMN "edited" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Attachment" DROP CONSTRAINT "Attachment_messageId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ALTER COLUMN "messageId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Message" ADD COLUMN "attachments" TEXT[];
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ADD COLUMN "communityId" TEXT;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Attachment" DROP CONSTRAINT "Attachment_communityId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Chunk" DROP CONSTRAINT "Chunk_attachmentId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Reaction" DROP CONSTRAINT "Reaction_messageId_fkey";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Chunk" ADD CONSTRAINT "Chunk_attachmentId_fkey" FOREIGN KEY ("attachmentId") REFERENCES "Attachment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `Reaction` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- You are about to drop the column `id` on the `Reaction` table. All the data in the column will be lost.
|
||||||
|
- A unique constraint covering the columns `[content]` on the table `Reaction` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_ReactionToUser" DROP CONSTRAINT "_ReactionToUser_A_fkey";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "Reaction_id_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Reaction" DROP CONSTRAINT "Reaction_pkey",
|
||||||
|
DROP COLUMN "id",
|
||||||
|
ADD CONSTRAINT "Reaction_pkey" PRIMARY KEY ("content");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Reaction_content_key" ON "Reaction"("content");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ReactionToUser" ADD CONSTRAINT "_ReactionToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Reaction"("content") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `Reaction` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- A unique constraint covering the columns `[id]` on the table `Reaction` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- The required column `id` was added to the `Reaction` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_ReactionToUser" DROP CONSTRAINT "_ReactionToUser_A_fkey";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "Reaction_content_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Reaction" DROP CONSTRAINT "Reaction_pkey",
|
||||||
|
ADD COLUMN "id" TEXT NOT NULL,
|
||||||
|
ADD CONSTRAINT "Reaction_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Reaction_id_key" ON "Reaction"("id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ReactionToUser" ADD CONSTRAINT "_ReactionToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Reaction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `fileName` to the `Attachment` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ADD COLUMN "fileName" TEXT NOT NULL;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ADD COLUMN "creationDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `attachments` on the `Message` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Message" DROP COLUMN "attachments";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `communityId` on table `Attachment` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `attachmentId` on table `Chunk` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ALTER COLUMN "communityId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Chunk" ALTER COLUMN "attachmentId" SET NOT NULL;
|
||||||
2
prisma/migrations/20260115135751_nickname/migration.sql
Normal file
2
prisma/migrations/20260115135751_nickname/migration.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "nickname" TEXT;
|
||||||
10
prisma/migrations/20260115140047_filename/migration.sql
Normal file
10
prisma/migrations/20260115140047_filename/migration.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `fileName` on the `Attachment` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `filename` to the `Attachment` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" DROP COLUMN "fileName",
|
||||||
|
ADD COLUMN "filename" TEXT NOT NULL;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `size` to the `Attachment` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ADD COLUMN "size" BIGINT NOT NULL;
|
||||||
12
prisma/migrations/20260116134610_chunks/migration.sql
Normal file
12
prisma/migrations/20260116134610_chunks/migration.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `finishedUploading` to the `Attachment` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `index` to the `Chunk` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ADD COLUMN "finishedUploading" BOOLEAN NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Chunk" ADD COLUMN "index" INTEGER NOT NULL;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Changed the type of `iv` on the `Chunk` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Chunk" DROP COLUMN "iv",
|
||||||
|
ADD COLUMN "iv" BYTEA NOT NULL;
|
||||||
10
prisma/migrations/20260116142142_mimetype/migration.sql
Normal file
10
prisma/migrations/20260116142142_mimetype/migration.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `mimeType` on the `Attachment` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `mimetype` to the `Attachment` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" DROP COLUMN "mimeType",
|
||||||
|
ADD COLUMN "mimetype" TEXT NOT NULL;
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `communityId` to the `Chunk` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Chunk" ADD COLUMN "communityId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Chunk" ADD CONSTRAINT "Chunk_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `messageId` on table `Attachment` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ALTER COLUMN "messageId" SET NOT NULL;
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Attachment" ALTER COLUMN "messageId" DROP NOT NULL;
|
||||||
|
|
@ -12,6 +12,7 @@ model Community {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
description String?
|
description String?
|
||||||
|
avatar String?
|
||||||
creationDate DateTime @default(now())
|
creationDate DateTime @default(now())
|
||||||
User User @relation(name: "OwnerCommunityToUser", fields: [ownerId], references: [id])
|
User User @relation(name: "OwnerCommunityToUser", fields: [ownerId], references: [id])
|
||||||
ownerId String
|
ownerId String
|
||||||
|
|
@ -19,6 +20,8 @@ model Community {
|
||||||
channels Channel[]
|
channels Channel[]
|
||||||
roles Role[]
|
roles Role[]
|
||||||
invites Invite[]
|
invites Invite[]
|
||||||
|
attachments Attachment[]
|
||||||
|
chunks Chunk[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Channel {
|
model Channel {
|
||||||
|
|
@ -45,9 +48,11 @@ model Role {
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
username String @unique
|
username String @unique
|
||||||
|
nickname String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
passwordHash String?
|
passwordHash String?
|
||||||
description String?
|
description String?
|
||||||
|
avatar String?
|
||||||
admin Boolean @default(false)
|
admin Boolean @default(false)
|
||||||
registerDate DateTime @default(now())
|
registerDate DateTime @default(now())
|
||||||
lastLogin DateTime?
|
lastLogin DateTime?
|
||||||
|
|
@ -57,6 +62,7 @@ model User {
|
||||||
communities Community[] @relation(name: "MembersCommunitiesToUsers")
|
communities Community[] @relation(name: "MembersCommunitiesToUsers")
|
||||||
roles Role[] @relation(name: "UsersRolesToUsers")
|
roles Role[] @relation(name: "UsersRolesToUsers")
|
||||||
messages Message[]
|
messages Message[]
|
||||||
|
reactions Reaction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
|
|
@ -87,11 +93,46 @@ model Message {
|
||||||
id String @id @unique @default(uuid())
|
id String @id @unique @default(uuid())
|
||||||
text String
|
text String
|
||||||
iv String?
|
iv String?
|
||||||
editHistory String[] @default([])
|
replyToId String?
|
||||||
edited Boolean @default(false)
|
edited Boolean @default(false)
|
||||||
|
editHistory String[] @default([])
|
||||||
|
reactions Reaction[]
|
||||||
|
attachments Attachment[]
|
||||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
ownerId String
|
ownerId String
|
||||||
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||||
channelId String
|
channelId String
|
||||||
creationDate DateTime @default(now())
|
creationDate DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Reaction {
|
||||||
|
id String @id @unique @default(uuid())
|
||||||
|
users User[]
|
||||||
|
content String
|
||||||
|
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||||
|
messageId String
|
||||||
|
}
|
||||||
|
|
||||||
|
model Attachment {
|
||||||
|
id String @id @unique @default(uuid())
|
||||||
|
filename String
|
||||||
|
mimetype String
|
||||||
|
size BigInt
|
||||||
|
chunks Chunk[]
|
||||||
|
finishedUploading Boolean
|
||||||
|
message Message? @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||||
|
messageId String?
|
||||||
|
community Community @relation(fields: [communityId], references: [id], onDelete: Cascade)
|
||||||
|
communityId String
|
||||||
|
creationDate DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model Chunk {
|
||||||
|
id String @id @unique @default(uuid())
|
||||||
|
iv Bytes
|
||||||
|
index Int
|
||||||
|
attachment Attachment @relation(fields: [attachmentId], references: [id], onDelete: Cascade)
|
||||||
|
attachmentId String
|
||||||
|
community Community @relation(fields: [communityId], references: [id], onDelete: Cascade)
|
||||||
|
communityId String
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
getChannelMessagesByIdAuth,
|
getChannelMessagesByIdAuth,
|
||||||
} from "../../services/channel/channel.js";
|
} from "../../services/channel/channel.js";
|
||||||
import { API_ERROR } from "../errors.js";
|
import { API_ERROR } from "../errors.js";
|
||||||
|
import { responseFullChannel } from "./helpers.js";
|
||||||
|
|
||||||
const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { id } = request.params as IGetChannelParams;
|
const { id } = request.params as IGetChannelParams;
|
||||||
|
|
@ -46,13 +47,7 @@ const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IGetChannelResponseError;
|
} as IGetChannelResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullChannel(channel) as IGetChannelResponseSuccess;
|
||||||
id: channel.id,
|
|
||||||
name: channel.name,
|
|
||||||
description: channel.description,
|
|
||||||
communityId: channel.communityId,
|
|
||||||
creationDate: channel.creationDate.getTime(),
|
|
||||||
} as IGetChannelResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const postCreateChannel = async (
|
const postCreateChannel = async (
|
||||||
|
|
@ -70,13 +65,7 @@ const postCreateChannel = async (
|
||||||
} as IPostCreateChannelResponseError;
|
} as IPostCreateChannelResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullChannel(channel) as IPostCreateChannelResponseSuccess;
|
||||||
id: channel.id,
|
|
||||||
name: channel.name,
|
|
||||||
description: channel.description,
|
|
||||||
communityId: channel.communityId,
|
|
||||||
creationDate: channel.creationDate.getTime(),
|
|
||||||
} as IPostCreateChannelResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
const patchChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -104,13 +93,7 @@ const patchChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IPatchChannelResponseError;
|
} as IPatchChannelResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullChannel(channel) as IPatchChannelResponseSuccess;
|
||||||
id: channel.id,
|
|
||||||
name: channel.name,
|
|
||||||
description: channel.description,
|
|
||||||
communityId: channel.communityId,
|
|
||||||
creationDate: channel.creationDate.getTime(),
|
|
||||||
} as IPatchChannelResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
const deleteChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -165,7 +148,10 @@ const getMessages = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
iv: message.iv,
|
iv: message.iv,
|
||||||
|
replyToId: message.replyToId,
|
||||||
edited: message.edited,
|
edited: message.edited,
|
||||||
|
reactions: message.reactions.map((reaction) => reaction.id),
|
||||||
|
attachments: message.attachments.map((attachment) => attachment.id),
|
||||||
ownerId: message.ownerId,
|
ownerId: message.ownerId,
|
||||||
creationDate: message.creationDate.getTime(),
|
creationDate: message.creationDate.getTime(),
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
13
src/controllers/channel/helpers.ts
Normal file
13
src/controllers/channel/helpers.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { Channel } from "../../generated/prisma/client.js";
|
||||||
|
import type { IChannel } from "./types.js";
|
||||||
|
|
||||||
|
const responseFullChannel = (channel: Channel) =>
|
||||||
|
({
|
||||||
|
id: channel.id,
|
||||||
|
name: channel.name,
|
||||||
|
description: channel.description,
|
||||||
|
communityId: channel.communityId,
|
||||||
|
creationDate: channel.creationDate.getTime(),
|
||||||
|
}) as IChannel;
|
||||||
|
|
||||||
|
export { responseFullChannel };
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./channel.js";
|
export * from "./channel.js";
|
||||||
export * from "./routes.js";
|
export * from "./routes.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
export * from "./helpers.js";
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,10 @@ interface IGetMessagesResponseMessage {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
|
replyToId?: string;
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
|
reactions: string[];
|
||||||
|
attachments: string[];
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import {
|
||||||
} 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";
|
||||||
|
import { responseFullCommunity } from "./helpers.js";
|
||||||
|
|
||||||
const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { id } = request.params as IGetCommunityParams;
|
const { id } = request.params as IGetCommunityParams;
|
||||||
|
|
@ -60,13 +61,7 @@ const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IGetCommunityResponseError;
|
} as IGetCommunityResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullCommunity(community) as IGetCommunityResponseSuccess;
|
||||||
id: community.id,
|
|
||||||
name: community.name,
|
|
||||||
description: community.description,
|
|
||||||
ownerId: community.ownerId,
|
|
||||||
creationDate: community.creationDate.getTime(),
|
|
||||||
} as IGetCommunityResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const postCreateCommunity = async (
|
const postCreateCommunity = async (
|
||||||
|
|
@ -87,13 +82,9 @@ const postCreateCommunity = async (
|
||||||
} as IPostCreateCommunityResponseError;
|
} as IPostCreateCommunityResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullCommunity(
|
||||||
id: community.id,
|
community,
|
||||||
name: community.name,
|
) as IPostCreateCommunityResponseSuccess;
|
||||||
description: community.description,
|
|
||||||
ownerId: community.ownerId,
|
|
||||||
creationDate: community.creationDate.getTime(),
|
|
||||||
} as IPostCreateCommunityResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -121,13 +112,7 @@ const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IPatchCommunityResponseError;
|
} as IPatchCommunityResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullCommunity(community) as IPatchCommunityResponseSuccess;
|
||||||
id: community.id,
|
|
||||||
name: community.name,
|
|
||||||
description: community.description,
|
|
||||||
ownerId: community.ownerId,
|
|
||||||
creationDate: community.creationDate.getTime(),
|
|
||||||
} as IPatchCommunityResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCommunity = async (
|
const deleteCommunity = async (
|
||||||
|
|
@ -185,6 +170,8 @@ const getMembers = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
members: members.map((member) => ({
|
members: members.map((member) => ({
|
||||||
id: member.id,
|
id: member.id,
|
||||||
username: member.username,
|
username: member.username,
|
||||||
|
nickname: member.nickname ?? member.username,
|
||||||
|
avatar: member.avatar,
|
||||||
})),
|
})),
|
||||||
} as IGetMembersResponseSuccess;
|
} as IGetMembersResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
14
src/controllers/community/helpers.ts
Normal file
14
src/controllers/community/helpers.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Community } from "../../generated/prisma/client.js";
|
||||||
|
import type { ICommunity } from "./types.js";
|
||||||
|
|
||||||
|
const responseFullCommunity = (community: Community) =>
|
||||||
|
({
|
||||||
|
id: community.id,
|
||||||
|
name: community.name,
|
||||||
|
description: community.description,
|
||||||
|
avatar: community.avatar,
|
||||||
|
ownerId: community.ownerId,
|
||||||
|
creationDate: community.creationDate.getTime(),
|
||||||
|
}) as ICommunity;
|
||||||
|
|
||||||
|
export { responseFullCommunity };
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./community.js";
|
export * from "./community.js";
|
||||||
export * from "./routes.js";
|
export * from "./routes.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
export * from "./helpers.js";
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ interface ICommunity {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
avatar?: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +79,8 @@ interface IGetMembersResponseSuccess {
|
||||||
interface IGetMembersResponseMember {
|
interface IGetMembersResponseMember {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
nickname?: string;
|
||||||
|
avatar?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGetChannelsParams {
|
interface IGetChannelsParams {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
enum API_ERROR {
|
enum API_ERROR {
|
||||||
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
|
||||||
NOT_FOUND = "NOT_FOUND",
|
NOT_FOUND = "NOT_FOUND",
|
||||||
ACCESS_DENIED = "ACCESS_DENIED",
|
ACCESS_DENIED = "ACCESS_DENIED",
|
||||||
|
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
||||||
|
FILE_INVALID = "FILE_INVALID",
|
||||||
}
|
}
|
||||||
|
|
||||||
export { API_ERROR };
|
export { API_ERROR };
|
||||||
|
|
|
||||||
379
src/controllers/file/file.ts
Normal file
379
src/controllers/file/file.ts
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||||
|
import type {
|
||||||
|
IGetUserAvatarParams,
|
||||||
|
IGetUserAvatarResponseError,
|
||||||
|
IPostUploadUserAvatarRequest,
|
||||||
|
IPostUploadUserAvatarResponseError,
|
||||||
|
IPostUploadUserAvatarResponseSuccess,
|
||||||
|
IGetCommunityAvatarParams,
|
||||||
|
IGetCommunityAvatarResponseError,
|
||||||
|
IPostUploadCommunityAvatarRequest,
|
||||||
|
IPostUploadCommunityAvatarResponseError,
|
||||||
|
IPostUploadCommunityAvatarResponseSuccess,
|
||||||
|
IGetAttachmentParams,
|
||||||
|
IGetAttachmentResponseError,
|
||||||
|
IGetAttachmentResponseSuccess,
|
||||||
|
IPostCreateAttachmentRequest,
|
||||||
|
IPostCreateAttachmentResponseError,
|
||||||
|
IPostCreateAttachmentResponseSuccess,
|
||||||
|
IGetFinishAttachmentParams,
|
||||||
|
IGetFinishAttachmentError,
|
||||||
|
IGetFinishAttachmentSuccess,
|
||||||
|
IGetChunkParams,
|
||||||
|
IGetChunkResponseError,
|
||||||
|
IPostUploadChunkParams,
|
||||||
|
IPostUploadChunkRequest,
|
||||||
|
IPostUploadChunkResponseError,
|
||||||
|
IPostUploadChunkResponseSuccess,
|
||||||
|
} from "./types.js";
|
||||||
|
import { API_ERROR } from "../errors.js";
|
||||||
|
import {
|
||||||
|
createUserAvatarAuth,
|
||||||
|
createCommunityAvatarAuth,
|
||||||
|
getAttachmentByIdAuth,
|
||||||
|
createAttachmentAuth,
|
||||||
|
getChunkByAttachmentIndexAuth,
|
||||||
|
createChunkAuth,
|
||||||
|
finishAttachmentByIdAuth,
|
||||||
|
} from "../../services/file/file.js";
|
||||||
|
import { join } from "path";
|
||||||
|
import { existsSync, createReadStream } from "fs";
|
||||||
|
import { lookup } from "mime-types";
|
||||||
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
||||||
|
import { responseFullAttachment } from "./helpers.js";
|
||||||
|
|
||||||
|
const getUserAvatar = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { filename } = request.params as IGetUserAvatarParams;
|
||||||
|
|
||||||
|
const filePath = join(process.cwd(), "files", "user", "avatars", filename);
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
filename: filename,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetUserAvatarResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeType = lookup(filename) || "application/octet-stream";
|
||||||
|
|
||||||
|
reply
|
||||||
|
.header("Content-Type", mimeType)
|
||||||
|
.header("Content-Disposition", `attachment; filename="${filename}"`);
|
||||||
|
return createReadStream(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const postUploadUserAvatar = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const { file } = request.body as IPostUploadUserAvatarRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
reply.status(400);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.FILE_INVALID,
|
||||||
|
} as IPostUploadUserAvatarResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await createUserAvatarAuth(file.filename, authHeader);
|
||||||
|
if (!user) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IPostUploadUserAvatarResponseError;
|
||||||
|
}
|
||||||
|
if (user === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IPostUploadUserAvatarResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadPath = join(
|
||||||
|
process.cwd(),
|
||||||
|
"files",
|
||||||
|
"user",
|
||||||
|
"avatars",
|
||||||
|
file.filename,
|
||||||
|
);
|
||||||
|
const uploadPathFolder = join(process.cwd(), "files", "user", "avatars");
|
||||||
|
await mkdir(uploadPathFolder, { recursive: true });
|
||||||
|
|
||||||
|
const fileBuffer = await file.toBuffer();
|
||||||
|
await writeFile(uploadPath, fileBuffer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: file.filename,
|
||||||
|
userId: user.id,
|
||||||
|
} as IPostUploadUserAvatarResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommunityAvatar = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const { filename } = request.params as IGetCommunityAvatarParams;
|
||||||
|
|
||||||
|
const filePath = join(
|
||||||
|
process.cwd(),
|
||||||
|
"files",
|
||||||
|
"community",
|
||||||
|
"avatars",
|
||||||
|
filename,
|
||||||
|
);
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
filename: filename,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetCommunityAvatarResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeType = lookup(filename) || "application/octet-stream";
|
||||||
|
|
||||||
|
reply
|
||||||
|
.header("Content-Type", mimeType)
|
||||||
|
.header("Content-Disposition", `attachment; filename="${filename}"`);
|
||||||
|
return createReadStream(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const postUploadCommunityAvatar = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const { communityId, file } =
|
||||||
|
request.body as IPostUploadCommunityAvatarRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
reply.status(400);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.FILE_INVALID,
|
||||||
|
} as IPostUploadUserAvatarResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const community = await createCommunityAvatarAuth(
|
||||||
|
file.filename,
|
||||||
|
communityId.value,
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
if (!community) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IPostUploadCommunityAvatarResponseError;
|
||||||
|
}
|
||||||
|
if (community === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IPostUploadCommunityAvatarResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadPathFolder = join(
|
||||||
|
process.cwd(),
|
||||||
|
"files",
|
||||||
|
"community",
|
||||||
|
"avatars",
|
||||||
|
);
|
||||||
|
await mkdir(uploadPathFolder, { recursive: true });
|
||||||
|
|
||||||
|
const uploadPath = join(uploadPathFolder, file.filename);
|
||||||
|
const fileBuffer = await file.toBuffer();
|
||||||
|
await writeFile(uploadPath, fileBuffer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: file.filename,
|
||||||
|
userId: community.id,
|
||||||
|
} as IPostUploadCommunityAvatarResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttachment = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { id } = request.params as IGetAttachmentParams;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const attachment = await getAttachmentByIdAuth(id, authHeader);
|
||||||
|
if (!attachment) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetAttachmentResponseError;
|
||||||
|
}
|
||||||
|
if (attachment === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IGetAttachmentResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseFullAttachment(attachment) as IGetAttachmentResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const postCreateAttachment = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const { filename, mimetype, size, communityId } =
|
||||||
|
request.body as IPostCreateAttachmentRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const attachment = await createAttachmentAuth(
|
||||||
|
{
|
||||||
|
filename: filename,
|
||||||
|
mimetype: mimetype,
|
||||||
|
size: size,
|
||||||
|
communityId: communityId,
|
||||||
|
},
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
if (!attachment) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IPostCreateAttachmentResponseError;
|
||||||
|
}
|
||||||
|
if (attachment === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IPostCreateAttachmentResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseFullAttachment({
|
||||||
|
...attachment,
|
||||||
|
chunks: [],
|
||||||
|
}) as IPostCreateAttachmentResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFinishAttachment = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const { id } = request.params as IGetFinishAttachmentParams;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const attachment = await finishAttachmentByIdAuth(id, authHeader);
|
||||||
|
if (!attachment) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetFinishAttachmentError;
|
||||||
|
}
|
||||||
|
if (attachment === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IGetFinishAttachmentError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: attachment.id,
|
||||||
|
} as IGetFinishAttachmentSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChunk = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { attachmentId, index } = request.params as IGetChunkParams;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const chunk = await getChunkByAttachmentIndexAuth(
|
||||||
|
attachmentId,
|
||||||
|
Number(index),
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
if (!chunk) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetChunkResponseError;
|
||||||
|
}
|
||||||
|
if (chunk === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IGetChunkResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePathFolder = join(process.cwd(), "files", "attachments");
|
||||||
|
const filePath = join(filePathFolder, `${chunk.id}.bin`);
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
console.log(filePath);
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IGetChunkResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await readFile(filePath);
|
||||||
|
const response = Buffer.concat([chunk.iv, file]);
|
||||||
|
|
||||||
|
reply.header("Content-Type", "application/octet-stream").send(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
const postUploadChunk = async (
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => {
|
||||||
|
const { attachmentId, index } = request.params as IPostUploadChunkParams;
|
||||||
|
const { iv, file } = request.body as IPostUploadChunkRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const ivBuffer = Buffer.from(iv.value, "base64");
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
reply.status(400);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.FILE_INVALID,
|
||||||
|
} as IPostUploadChunkResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk = await createChunkAuth(
|
||||||
|
{
|
||||||
|
attachmentId: attachmentId,
|
||||||
|
index: Number(index),
|
||||||
|
iv: ivBuffer,
|
||||||
|
},
|
||||||
|
authHeader,
|
||||||
|
);
|
||||||
|
if (!chunk) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IPostUploadChunkResponseError;
|
||||||
|
}
|
||||||
|
if (chunk === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IPostUploadChunkResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadPathFolder = join(process.cwd(), "files", "attachments");
|
||||||
|
await mkdir(uploadPathFolder, { recursive: true });
|
||||||
|
|
||||||
|
const uploadPath = join(uploadPathFolder, `${chunk.id}.bin`);
|
||||||
|
const fileBuffer = await file.toBuffer();
|
||||||
|
await writeFile(uploadPath, fileBuffer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: chunk.id,
|
||||||
|
index: chunk.index,
|
||||||
|
attachmentId: chunk.attachmentId,
|
||||||
|
} as IPostUploadChunkResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getUserAvatar,
|
||||||
|
postUploadUserAvatar,
|
||||||
|
getCommunityAvatar,
|
||||||
|
postUploadCommunityAvatar,
|
||||||
|
getAttachment,
|
||||||
|
postCreateAttachment,
|
||||||
|
getFinishAttachment,
|
||||||
|
getChunk,
|
||||||
|
postUploadChunk,
|
||||||
|
};
|
||||||
16
src/controllers/file/helpers.ts
Normal file
16
src/controllers/file/helpers.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { AttachmentWithChunks } from "../../services/file/types.js";
|
||||||
|
import type { IAttachment } from "./types.js";
|
||||||
|
|
||||||
|
const responseFullAttachment = (attachment: AttachmentWithChunks) =>
|
||||||
|
({
|
||||||
|
id: attachment.id,
|
||||||
|
filename: attachment.filename,
|
||||||
|
mimetype: attachment.mimetype,
|
||||||
|
size: Number(attachment.size),
|
||||||
|
messageId: attachment.messageId,
|
||||||
|
chunks: attachment.chunks.map((chunk) => chunk.id),
|
||||||
|
finishedUploading: attachment.finishedUploading,
|
||||||
|
creationDate: attachment.creationDate.getTime(),
|
||||||
|
}) as IAttachment;
|
||||||
|
|
||||||
|
export { responseFullAttachment };
|
||||||
4
src/controllers/file/index.ts
Normal file
4
src/controllers/file/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./file.js";
|
||||||
|
export * from "./routes.js";
|
||||||
|
export * from "./types.js";
|
||||||
|
export * from "./helpers.js";
|
||||||
22
src/controllers/file/routes.ts
Normal file
22
src/controllers/file/routes.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { type FastifyInstance } from "fastify";
|
||||||
|
import * as controller from "./file.js";
|
||||||
|
|
||||||
|
const fileRoutes = async (fastify: FastifyInstance) => {
|
||||||
|
fastify.get(`/avatar/user/:filename`, controller.getUserAvatar);
|
||||||
|
fastify.post(`/avatar/user/upload`, controller.postUploadUserAvatar);
|
||||||
|
fastify.get(`/avatar/community/:filename`, controller.getCommunityAvatar);
|
||||||
|
fastify.post(
|
||||||
|
`/avatar/community/upload`,
|
||||||
|
controller.postUploadCommunityAvatar,
|
||||||
|
);
|
||||||
|
fastify.get(`/attachment/:id`, controller.getAttachment);
|
||||||
|
fastify.post(`/attachment`, controller.postCreateAttachment);
|
||||||
|
fastify.get(`/attachment/:id/finish`, controller.getFinishAttachment);
|
||||||
|
fastify.get(`/attachment/:attachmentId/chunk/:index`, controller.getChunk);
|
||||||
|
fastify.post(
|
||||||
|
`/attachment/:attachmentId/chunk/:index`,
|
||||||
|
controller.postUploadChunk,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { fileRoutes };
|
||||||
156
src/controllers/file/types.ts
Normal file
156
src/controllers/file/types.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
import type { MultipartFile, MultipartValue } from "@fastify/multipart";
|
||||||
|
import type { API_ERROR } from "../errors.js";
|
||||||
|
|
||||||
|
interface IGetUserAvatarParams {
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetUserAvatarResponseError {
|
||||||
|
filename: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadUserAvatarRequest {
|
||||||
|
file: MultipartFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadUserAvatarResponseError {
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadUserAvatarResponseSuccess {
|
||||||
|
filename: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetCommunityAvatarParams {
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetCommunityAvatarResponseError {
|
||||||
|
filename: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadCommunityAvatarRequest {
|
||||||
|
communityId: MultipartValue<string>;
|
||||||
|
file: MultipartFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadCommunityAvatarResponseError {
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadCommunityAvatarResponseSuccess {
|
||||||
|
filename: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAttachment {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
mimetype: string;
|
||||||
|
size: number;
|
||||||
|
messageId: string;
|
||||||
|
chunks: string[];
|
||||||
|
finishedUploading: boolean;
|
||||||
|
creationDate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetAttachmentParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetAttachmentResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetAttachmentResponseSuccess extends IAttachment {}
|
||||||
|
|
||||||
|
interface IPostCreateAttachmentRequest {
|
||||||
|
filename: string;
|
||||||
|
mimetype: string;
|
||||||
|
size: number;
|
||||||
|
communityId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostCreateAttachmentResponseError {
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostCreateAttachmentResponseSuccess extends IAttachment {}
|
||||||
|
|
||||||
|
interface IGetFinishAttachmentParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetFinishAttachmentError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetFinishAttachmentSuccess {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IChunk {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
attachmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetChunkParams {
|
||||||
|
attachmentId: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetChunkResponseError {
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadChunkParams {
|
||||||
|
attachmentId: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadChunkRequest {
|
||||||
|
iv: MultipartValue<string>;
|
||||||
|
file: MultipartFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadChunkResponseError {
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPostUploadChunkResponseSuccess extends IChunk {}
|
||||||
|
|
||||||
|
export {
|
||||||
|
type IGetUserAvatarParams,
|
||||||
|
type IGetUserAvatarResponseError,
|
||||||
|
type IPostUploadUserAvatarRequest,
|
||||||
|
type IPostUploadUserAvatarResponseError,
|
||||||
|
type IPostUploadUserAvatarResponseSuccess,
|
||||||
|
type IGetCommunityAvatarParams,
|
||||||
|
type IGetCommunityAvatarResponseError,
|
||||||
|
type IPostUploadCommunityAvatarRequest,
|
||||||
|
type IPostUploadCommunityAvatarResponseError,
|
||||||
|
type IPostUploadCommunityAvatarResponseSuccess,
|
||||||
|
type IAttachment,
|
||||||
|
type IGetAttachmentParams,
|
||||||
|
type IGetAttachmentResponseError,
|
||||||
|
type IGetAttachmentResponseSuccess,
|
||||||
|
type IPostCreateAttachmentRequest,
|
||||||
|
type IPostCreateAttachmentResponseError,
|
||||||
|
type IPostCreateAttachmentResponseSuccess,
|
||||||
|
type IGetFinishAttachmentParams,
|
||||||
|
type IGetFinishAttachmentError,
|
||||||
|
type IGetFinishAttachmentSuccess,
|
||||||
|
type IChunk,
|
||||||
|
type IGetChunkParams,
|
||||||
|
type IGetChunkResponseError,
|
||||||
|
type IPostUploadChunkParams,
|
||||||
|
type IPostUploadChunkRequest,
|
||||||
|
type IPostUploadChunkResponseError,
|
||||||
|
type IPostUploadChunkResponseSuccess,
|
||||||
|
};
|
||||||
19
src/controllers/message/helpers.ts
Normal file
19
src/controllers/message/helpers.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { FullMessage } from "../../services/message/types.js";
|
||||||
|
import type { IMessage } from "./types.js";
|
||||||
|
|
||||||
|
const responseFullMessage = (message: FullMessage) =>
|
||||||
|
({
|
||||||
|
id: message.id,
|
||||||
|
text: message.text,
|
||||||
|
iv: message.iv,
|
||||||
|
replyToId: message.replyToId,
|
||||||
|
edited: message.edited,
|
||||||
|
editHistory: message.editHistory,
|
||||||
|
reactions: message.reactions.map((reaction) => reaction.id),
|
||||||
|
attachments: message.attachments.map((attachment) => attachment.id),
|
||||||
|
ownerId: message.ownerId,
|
||||||
|
channelId: message.channelId,
|
||||||
|
creationDate: message.creationDate.getTime(),
|
||||||
|
}) as IMessage;
|
||||||
|
|
||||||
|
export { responseFullMessage };
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./message.js";
|
export * from "./message.js";
|
||||||
export * from "./routes.js";
|
export * from "./routes.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
export * from "./helpers.js";
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,25 @@ import type {
|
||||||
IDeleteMessageParams,
|
IDeleteMessageParams,
|
||||||
IDeleteMessageResponseError,
|
IDeleteMessageResponseError,
|
||||||
IDeleteMessageResponseSuccess,
|
IDeleteMessageResponseSuccess,
|
||||||
|
IReactMessageParams,
|
||||||
|
IReactMessageRequest,
|
||||||
|
IReactMessageResponseError,
|
||||||
|
IReactMessageResponseSuccess,
|
||||||
|
IUnreactMessageParams,
|
||||||
|
IUnreactMessageRequest,
|
||||||
|
IUnreactMessageResponseError,
|
||||||
|
IUnreactMessageResponseSuccess,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import {
|
import {
|
||||||
createMessageAuth,
|
createMessageAuth,
|
||||||
deleteMessageByIdAuth,
|
deleteMessageByIdAuth,
|
||||||
getMessageByIdAuth,
|
getMessageByIdAuth,
|
||||||
updateMessageByIdAuth,
|
updateMessageByIdAuth,
|
||||||
|
reactMessageByIdAuth,
|
||||||
|
unreactMessageByIdAuth,
|
||||||
} from "../../services/message/message.js";
|
} from "../../services/message/message.js";
|
||||||
import { API_ERROR } from "../errors.js";
|
import { API_ERROR } from "../errors.js";
|
||||||
|
import { responseFullMessage } from "./helpers.js";
|
||||||
|
|
||||||
const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { id } = request.params as IGetMessageParams;
|
const { id } = request.params as IGetMessageParams;
|
||||||
|
|
@ -42,16 +53,7 @@ const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IGetMessageResponseError;
|
} as IGetMessageResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullMessage(message) as IGetMessageResponseSuccess;
|
||||||
id: message.id,
|
|
||||||
text: message.text,
|
|
||||||
iv: message.iv,
|
|
||||||
editHistory: message.editHistory,
|
|
||||||
edited: message.edited,
|
|
||||||
ownerId: message.ownerId,
|
|
||||||
channelId: message.channelId,
|
|
||||||
creationDate: message.creationDate.getTime(),
|
|
||||||
} as IGetMessageResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const postCreateMessage = async (
|
const postCreateMessage = async (
|
||||||
|
|
@ -69,16 +71,7 @@ const postCreateMessage = async (
|
||||||
} as IPostCreateMessageResponseError;
|
} as IPostCreateMessageResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullMessage(message) as IPostCreateMessageResponseSuccess;
|
||||||
id: message.id,
|
|
||||||
text: message.text,
|
|
||||||
iv: message.iv,
|
|
||||||
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 patchMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -106,15 +99,7 @@ const patchMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IPatchMessageResponseError;
|
} as IPatchMessageResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullMessage(message) as IPatchMessageResponseSuccess;
|
||||||
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 deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -144,4 +129,59 @@ const deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IDeleteMessageResponseSuccess;
|
} as IDeleteMessageResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getMessage, postCreateMessage, patchMessage, deleteMessage };
|
const reactMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { id } = request.params as IReactMessageParams;
|
||||||
|
const { content } = request.body as IReactMessageRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const message = await reactMessageByIdAuth(id, content, authHeader);
|
||||||
|
if (!message) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IReactMessageResponseError;
|
||||||
|
}
|
||||||
|
if (message === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IReactMessageResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseFullMessage(message) as IReactMessageResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreactMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
const { id } = request.params as IUnreactMessageParams;
|
||||||
|
const { content } = request.body as IUnreactMessageRequest;
|
||||||
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
||||||
|
const message = await unreactMessageByIdAuth(id, content, authHeader);
|
||||||
|
if (!message) {
|
||||||
|
reply.status(404);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.NOT_FOUND,
|
||||||
|
} as IReactMessageResponseError;
|
||||||
|
}
|
||||||
|
if (message === API_ERROR.ACCESS_DENIED) {
|
||||||
|
reply.status(403);
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
error: API_ERROR.ACCESS_DENIED,
|
||||||
|
} as IReactMessageResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseFullMessage(message) as IUnreactMessageResponseSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getMessage,
|
||||||
|
postCreateMessage,
|
||||||
|
patchMessage,
|
||||||
|
deleteMessage,
|
||||||
|
reactMessage,
|
||||||
|
unreactMessage,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ const messageRoutes = async (fastify: FastifyInstance) => {
|
||||||
fastify.post(`/`, controller.postCreateMessage);
|
fastify.post(`/`, controller.postCreateMessage);
|
||||||
fastify.patch(`/:id`, controller.patchMessage);
|
fastify.patch(`/:id`, controller.patchMessage);
|
||||||
fastify.delete(`/:id`, controller.deleteMessage);
|
fastify.delete(`/:id`, controller.deleteMessage);
|
||||||
|
fastify.patch(`/:id/react`, controller.reactMessage);
|
||||||
|
fastify.patch(`/:id/unreact`, controller.unreactMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { messageRoutes };
|
export { messageRoutes };
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ interface IMessage {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
editHistory: string[];
|
replyToId?: string;
|
||||||
edited: boolean;
|
edited: boolean;
|
||||||
|
editHistory: string[];
|
||||||
|
reactions: string[];
|
||||||
|
attachments: string[];
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
|
|
@ -26,6 +29,8 @@ interface IPostCreateMessageRequest {
|
||||||
text: string;
|
text: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
replyToId?: string;
|
||||||
|
attachments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPostCreateMessageResponseError {
|
interface IPostCreateMessageResponseError {
|
||||||
|
|
@ -65,6 +70,36 @@ interface IDeleteMessageResponseSuccess {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IReactMessageParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReactMessageRequest {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReactMessageResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReactMessageResponseSuccess extends IMessage {}
|
||||||
|
|
||||||
|
interface IUnreactMessageParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUnreactMessageRequest {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUnreactMessageResponseError {
|
||||||
|
id: string;
|
||||||
|
error: API_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUnreactMessageResponseSuccess extends IMessage {}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type IMessage,
|
type IMessage,
|
||||||
type IGetMessageParams,
|
type IGetMessageParams,
|
||||||
|
|
@ -80,4 +115,12 @@ export {
|
||||||
type IDeleteMessageParams,
|
type IDeleteMessageParams,
|
||||||
type IDeleteMessageResponseError,
|
type IDeleteMessageResponseError,
|
||||||
type IDeleteMessageResponseSuccess,
|
type IDeleteMessageResponseSuccess,
|
||||||
|
type IReactMessageParams,
|
||||||
|
type IReactMessageRequest,
|
||||||
|
type IReactMessageResponseError,
|
||||||
|
type IReactMessageResponseSuccess,
|
||||||
|
type IUnreactMessageParams,
|
||||||
|
type IUnreactMessageRequest,
|
||||||
|
type IUnreactMessageResponseError,
|
||||||
|
type IUnreactMessageResponseSuccess,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
14
src/controllers/role/helpers.ts
Normal file
14
src/controllers/role/helpers.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Role } from "../../generated/prisma/client.js";
|
||||||
|
import type { IRole } from "./types.js";
|
||||||
|
|
||||||
|
const responseFullRole = (role: Role) =>
|
||||||
|
({
|
||||||
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
|
description: role.description,
|
||||||
|
communityId: role.communityId,
|
||||||
|
permissions: role.permissions,
|
||||||
|
creationDate: role.creationDate.getTime(),
|
||||||
|
}) as IRole;
|
||||||
|
|
||||||
|
export { responseFullRole };
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./role.js";
|
export * from "./role.js";
|
||||||
export * from "./routes.js";
|
export * from "./routes.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
export * from "./helpers.js";
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
updateRoleByIdAuth,
|
updateRoleByIdAuth,
|
||||||
} from "../../services/role/role.js";
|
} from "../../services/role/role.js";
|
||||||
import { API_ERROR } from "../errors.js";
|
import { API_ERROR } from "../errors.js";
|
||||||
|
import { responseFullRole } from "./helpers.js";
|
||||||
|
|
||||||
const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const { id } = request.params as IGetRoleParams;
|
const { id } = request.params as IGetRoleParams;
|
||||||
|
|
@ -52,14 +53,7 @@ const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IGetRoleResponseError;
|
} as IGetRoleResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullRole(role) as IGetRoleResponseSuccess;
|
||||||
id: role.id,
|
|
||||||
name: role.name,
|
|
||||||
description: role.description,
|
|
||||||
communityId: role.communityId,
|
|
||||||
permissions: role.permissions,
|
|
||||||
creationDate: role.creationDate.getTime(),
|
|
||||||
} as IGetRoleResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -74,14 +68,7 @@ const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IPostCreateRoleResponseError;
|
} as IPostCreateRoleResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullRole(role) as IPostCreateRoleResponseSuccess;
|
||||||
id: role.id,
|
|
||||||
name: role.name,
|
|
||||||
description: role.description,
|
|
||||||
communityId: role.communityId,
|
|
||||||
permissions: role.permissions,
|
|
||||||
creationDate: role.creationDate.getTime(),
|
|
||||||
} as IPostCreateRoleResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
const patchRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -105,14 +92,7 @@ const patchRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IPatchRoleResponseError;
|
} as IPatchRoleResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullRole(role) as IPatchRoleResponseSuccess;
|
||||||
id: role.id,
|
|
||||||
name: role.name,
|
|
||||||
description: role.description,
|
|
||||||
communityId: role.communityId,
|
|
||||||
permissions: role.permissions,
|
|
||||||
creationDate: role.creationDate.getTime(),
|
|
||||||
} as IPatchRoleResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
const deleteRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ interface IRole {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
communityId: string;
|
communityId: string;
|
||||||
permissions: PERMISSION[];
|
permissions: string[];
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
src/controllers/user/helpers.ts
Normal file
17
src/controllers/user/helpers.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { User } from "../../generated/prisma/client.js";
|
||||||
|
import type { IUser } from "./types.js";
|
||||||
|
|
||||||
|
const responseFullUser = (user: User) =>
|
||||||
|
({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
nickname: user.nickname ?? undefined,
|
||||||
|
email: user.email,
|
||||||
|
description: user.description,
|
||||||
|
avatar: user.avatar,
|
||||||
|
admin: user.admin ?? false,
|
||||||
|
registerDate: user.registerDate.getTime(),
|
||||||
|
lastLogin: user.lastLogin?.getTime() ?? 0,
|
||||||
|
}) as IUser;
|
||||||
|
|
||||||
|
export { responseFullUser };
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./user.js";
|
export * from "./user.js";
|
||||||
export * from "./routes.js";
|
export * from "./routes.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
export * from "./helpers.js";
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ import type { API_ERROR } from "../errors.js";
|
||||||
interface IUser {
|
interface IUser {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
nickname?: string;
|
||||||
description: string;
|
email?: string;
|
||||||
|
description?: string;
|
||||||
|
avatar?: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
registerDate: number;
|
registerDate: number;
|
||||||
lastLogin: number;
|
lastLogin: number;
|
||||||
|
|
@ -49,6 +51,7 @@ interface IPatchUserParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPatchUserRequest {
|
interface IPatchUserRequest {
|
||||||
|
nickname?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +117,7 @@ interface IGetCommunitiesResponseCommunity {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
avatar?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
getLoggedUserAuth,
|
getLoggedUserAuth,
|
||||||
} from "../../services/user/user.js";
|
} from "../../services/user/user.js";
|
||||||
import { API_ERROR } from "../errors.js";
|
import { API_ERROR } from "../errors.js";
|
||||||
|
import { responseFullUser } from "./helpers.js";
|
||||||
|
|
||||||
const getUserLogged = async (request: FastifyRequest, reply: FastifyReply) => {
|
const getUserLogged = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
const authHeader = request.headers["authorization"];
|
const authHeader = request.headers["authorization"];
|
||||||
|
|
@ -69,15 +70,7 @@ const getUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IGetUserResponseError;
|
} as IGetUserResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullUser(user) as IGetUserResponseSuccess;
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
description: user.description,
|
|
||||||
admin: user.admin,
|
|
||||||
registerDate: user.registerDate.getTime(),
|
|
||||||
lastLogin: user.lastLogin?.getTime() ?? 0,
|
|
||||||
} as IGetUserResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const postCreateUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
const postCreateUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -92,15 +85,7 @@ const postCreateUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IPostCreateUserResponseError;
|
} as IPostCreateUserResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullUser(user) as IPostCreateUserResponseSuccess;
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
description: user.description,
|
|
||||||
admin: user.admin,
|
|
||||||
registerDate: user.registerDate.getTime(),
|
|
||||||
lastLogin: user.lastLogin?.getTime() ?? 0,
|
|
||||||
} as IPostCreateUserResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
const patchUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -124,15 +109,7 @@ const patchUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
} as IPatchUserResponseError;
|
} as IPatchUserResponseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return responseFullUser(user) as IPatchUserResponseSuccess;
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
description: user.description,
|
|
||||||
admin: user.admin,
|
|
||||||
registerDate: user.registerDate.getTime(),
|
|
||||||
lastLogin: user.lastLogin?.getTime() ?? 0,
|
|
||||||
} as IPatchUserResponseSuccess;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
const deleteUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
|
@ -219,6 +196,7 @@ const getCommunities = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
id: community.id,
|
id: community.id,
|
||||||
name: community.name,
|
name: community.name,
|
||||||
description: community.description,
|
description: community.description,
|
||||||
|
avatar: community.avatar,
|
||||||
})),
|
})),
|
||||||
} as IGetCommunitiesResponseSuccess;
|
} as IGetCommunitiesResponseSuccess;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
16
src/index.ts
16
src/index.ts
|
|
@ -2,6 +2,7 @@ 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 websocket from "@fastify/websocket";
|
||||||
|
import multipart from "@fastify/multipart";
|
||||||
|
|
||||||
import { config } from "./config.js";
|
import { config } from "./config.js";
|
||||||
|
|
||||||
|
|
@ -16,7 +17,9 @@ 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 { messageRoutes } from "./controllers/message/routes.js";
|
||||||
|
import { fileRoutes } from "./controllers/file/routes.js";
|
||||||
import { websocketRoutes } from "./controllers/websocket/routes.js";
|
import { websocketRoutes } from "./controllers/websocket/routes.js";
|
||||||
|
|
||||||
import { initializeCommunitiesWithReadableUsersCache } from "./services/user/user.js";
|
import { initializeCommunitiesWithReadableUsersCache } from "./services/user/user.js";
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
|
|
@ -28,10 +31,18 @@ app.register(cors, {
|
||||||
credentials: true,
|
credentials: true,
|
||||||
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(cookie, { secret: getCookieSecret() });
|
app.register(cookie, { secret: getCookieSecret() });
|
||||||
|
|
||||||
app.register(websocket);
|
app.register(websocket);
|
||||||
|
app.register(multipart, {
|
||||||
|
attachFieldsToBody: true,
|
||||||
|
sharedSchemaId: "MultipartFileType",
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024,
|
||||||
|
files: 1,
|
||||||
|
fields: 10,
|
||||||
|
fieldSize: 1024 * 1024,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
app.register(testRoutes);
|
app.register(testRoutes);
|
||||||
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
||||||
|
|
@ -42,6 +53,7 @@ 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(messageRoutes, { prefix: "/api/v1/message" });
|
||||||
|
app.register(fileRoutes, { prefix: "/api/v1/file" });
|
||||||
app.register(websocketRoutes, { prefix: "/ws" });
|
app.register(websocketRoutes, { prefix: "/ws" });
|
||||||
|
|
||||||
app.listen({ port: config.port }, (err, address) => {
|
app.listen({ port: config.port }, (err, address) => {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ const registerUser = async (
|
||||||
newUser = await getDB().user.create({
|
newUser = await getDB().user.create({
|
||||||
data: {
|
data: {
|
||||||
username: registration.username,
|
username: registration.username,
|
||||||
|
nickname: registration.username,
|
||||||
passwordHash: passwordHash,
|
passwordHash: passwordHash,
|
||||||
email: registration.email ?? null,
|
email: registration.email ?? null,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { API_ERROR } from "../../controllers/errors.js";
|
import { API_ERROR } from "../../controllers/errors.js";
|
||||||
import type { Channel, Message } from "../../generated/prisma/client.js";
|
import type { Channel } 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";
|
||||||
import { getCommunityById } from "../community/community.js";
|
import { getCommunityById } from "../community/community.js";
|
||||||
|
import type { FullMessage } from "../message/types.js";
|
||||||
import { getUserIdsInCommunity } from "../user/user.js";
|
import { getUserIdsInCommunity } from "../user/user.js";
|
||||||
import { SocketMessageTypes } from "../websocket/types.js";
|
import { SocketMessageTypes } from "../websocket/types.js";
|
||||||
import { sendMessageToUsersWS } from "../websocket/websocket.js";
|
import { sendMessageToUsersWS } from "../websocket/websocket.js";
|
||||||
|
|
@ -174,8 +175,20 @@ const deleteChannelByIdAuth = async (
|
||||||
|
|
||||||
const getChannelMessagesById = async (
|
const getChannelMessagesById = async (
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<Message[] | null> => {
|
): Promise<FullMessage[] | null> => {
|
||||||
return await getDB().message.findMany({
|
return await getDB().message.findMany({
|
||||||
|
include: {
|
||||||
|
reactions: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
where: {
|
where: {
|
||||||
channelId: id,
|
channelId: id,
|
||||||
},
|
},
|
||||||
|
|
@ -185,7 +198,7 @@ const getChannelMessagesById = async (
|
||||||
const getChannelMessagesByIdAuth = async (
|
const getChannelMessagesByIdAuth = async (
|
||||||
id: string,
|
id: string,
|
||||||
authHeader: string | undefined,
|
authHeader: string | undefined,
|
||||||
): Promise<Message[] | null | API_ERROR.ACCESS_DENIED> => {
|
): Promise<FullMessage[] | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
const authUser = await getUserFromAuth(authHeader);
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
const channel = await getChannelById(id);
|
const channel = await getChannelById(id);
|
||||||
const community = await getCommunityById(channel?.communityId ?? "");
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ const getCommunityMembersById = async (
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
|
nickname: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ interface IUpdateCommunity {
|
||||||
interface ICommunityMember {
|
interface ICommunityMember {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
nickname?: string | null;
|
||||||
|
avatar?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICommunityChannel {
|
interface ICommunityChannel {
|
||||||
|
|
|
||||||
308
src/services/file/file.ts
Normal file
308
src/services/file/file.ts
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
import { API_ERROR } from "../../controllers/errors.js";
|
||||||
|
import type {
|
||||||
|
Attachment,
|
||||||
|
Chunk,
|
||||||
|
Community,
|
||||||
|
User,
|
||||||
|
} from "../../generated/prisma/client.js";
|
||||||
|
import { getDB } from "../../store/store.js";
|
||||||
|
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
||||||
|
import { PERMISSION } from "../auth/permission.js";
|
||||||
|
import { getCommunityById } from "../community/community.js";
|
||||||
|
import type {
|
||||||
|
AttachmentWithChunks,
|
||||||
|
ICreateAttachment,
|
||||||
|
ICreateChunk,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
const createUserAvatar = async (
|
||||||
|
ownerId: string,
|
||||||
|
filename: string,
|
||||||
|
): Promise<User | null> => {
|
||||||
|
return await getDB().user.update({
|
||||||
|
where: {
|
||||||
|
id: ownerId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
avatar: filename,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUserAvatarAuth = async (
|
||||||
|
filename: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<User | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
|
||||||
|
if (!authUser) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createUserAvatar(authUser.id, filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCommunityAvatar = async (
|
||||||
|
communityId: string,
|
||||||
|
filename: string,
|
||||||
|
): Promise<Community | null> => {
|
||||||
|
return await getDB().community.update({
|
||||||
|
where: {
|
||||||
|
id: communityId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
avatar: filename,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCommunityAvatarAuth = async (
|
||||||
|
filename: string,
|
||||||
|
communityId: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Community | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const community = await getCommunityById(communityId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!authUser ||
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.COMMUNITY_MANAGE],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createCommunityAvatar(communityId, filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttachmentById = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<AttachmentWithChunks | null> => {
|
||||||
|
return await getDB().attachment.findUnique({
|
||||||
|
where: { id: id },
|
||||||
|
include: {
|
||||||
|
chunks: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttachmentByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<AttachmentWithChunks | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const attachment = await getAttachmentById(id);
|
||||||
|
const community = await getCommunityById(attachment?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_READ],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAttachment = async (
|
||||||
|
create: ICreateAttachment,
|
||||||
|
): Promise<Attachment> => {
|
||||||
|
return await getDB().attachment.create({
|
||||||
|
data: {
|
||||||
|
...create,
|
||||||
|
finishedUploading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAttachmentAuth = async (
|
||||||
|
create: ICreateAttachment,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Attachment | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const community = await getCommunityById(create.communityId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_CREATE],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createAttachment(create);
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishAttachmentById = async (id: string): Promise<Attachment | null> => {
|
||||||
|
return await getDB().attachment.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
finishedUploading: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishAttachmentByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Attachment | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const attachment = await getAttachmentById(id);
|
||||||
|
const community = await getCommunityById(attachment?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_CREATE],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishAttachmentById(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChunkById = async (id: string): Promise<Chunk | null> => {
|
||||||
|
return await getDB().chunk.findUnique({
|
||||||
|
where: { id: id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChunkByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Chunk | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const chunk = await getChunkById(id);
|
||||||
|
const community = await getCommunityById(chunk?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_READ],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunk;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChunkByAttachmentIndex = async (
|
||||||
|
attachmentId: string,
|
||||||
|
index: number,
|
||||||
|
): Promise<Chunk | null> => {
|
||||||
|
return await getDB().chunk.findFirst({
|
||||||
|
where: { attachmentId: attachmentId, index: index },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChunkByAttachmentIndexAuth = async (
|
||||||
|
attachmentId: string,
|
||||||
|
index: number,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Chunk | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const chunk = await getChunkByAttachmentIndex(attachmentId, index);
|
||||||
|
const community = await getCommunityById(chunk?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_READ],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunk;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChunk = async (
|
||||||
|
communityId: string,
|
||||||
|
create: ICreateChunk,
|
||||||
|
): Promise<Chunk> => {
|
||||||
|
return await getDB().chunk.create({
|
||||||
|
data: {
|
||||||
|
communityId: communityId,
|
||||||
|
...create,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChunkAuth = async (
|
||||||
|
create: ICreateChunk,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<Chunk | API_ERROR.ACCESS_DENIED> => {
|
||||||
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
|
const attachment = await getAttachmentById(create.attachmentId);
|
||||||
|
const community = await getCommunityById(attachment?.communityId ?? "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!community ||
|
||||||
|
!(await isUserAllowed(
|
||||||
|
authUser,
|
||||||
|
{
|
||||||
|
community: community,
|
||||||
|
},
|
||||||
|
community,
|
||||||
|
[PERMISSION.MESSAGES_CREATE],
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createChunk(community.id, create);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
createUserAvatar,
|
||||||
|
createUserAvatarAuth,
|
||||||
|
createCommunityAvatar,
|
||||||
|
createCommunityAvatarAuth,
|
||||||
|
getAttachmentById,
|
||||||
|
getAttachmentByIdAuth,
|
||||||
|
createAttachment,
|
||||||
|
createAttachmentAuth,
|
||||||
|
finishAttachmentById,
|
||||||
|
finishAttachmentByIdAuth,
|
||||||
|
getChunkById,
|
||||||
|
getChunkByIdAuth,
|
||||||
|
getChunkByAttachmentIndex,
|
||||||
|
getChunkByAttachmentIndexAuth,
|
||||||
|
createChunk,
|
||||||
|
createChunkAuth,
|
||||||
|
};
|
||||||
2
src/services/file/index.ts
Normal file
2
src/services/file/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./file.js";
|
||||||
|
export * from "./types.js";
|
||||||
22
src/services/file/types.ts
Normal file
22
src/services/file/types.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { Attachment } from "../../generated/prisma/client.js";
|
||||||
|
|
||||||
|
interface AttachmentWithChunks extends Attachment {
|
||||||
|
chunks: {
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICreateAttachment {
|
||||||
|
filename: string;
|
||||||
|
mimetype: string;
|
||||||
|
size: number;
|
||||||
|
communityId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICreateChunk {
|
||||||
|
iv: Buffer<ArrayBuffer>;
|
||||||
|
index: number;
|
||||||
|
attachmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { type AttachmentWithChunks, type ICreateAttachment, type ICreateChunk };
|
||||||
|
|
@ -13,10 +13,22 @@ import { getCommunityById } from "../community/community.js";
|
||||||
import { getUserIdsInCommunityReadMessagesPermission } from "../user/user.js";
|
import { getUserIdsInCommunityReadMessagesPermission } from "../user/user.js";
|
||||||
import { SocketMessageTypes } from "../websocket/types.js";
|
import { SocketMessageTypes } from "../websocket/types.js";
|
||||||
import { sendMessageToUsersWS } from "../websocket/websocket.js";
|
import { sendMessageToUsersWS } from "../websocket/websocket.js";
|
||||||
import type { ICreateMessage, IUpdateMessage } from "./types.js";
|
import type { FullMessage, ICreateMessage, IUpdateMessage } from "./types.js";
|
||||||
|
|
||||||
const getMessageById = async (id: string): Promise<Message | null> => {
|
const getMessageById = async (id: string): Promise<FullMessage | null> => {
|
||||||
return await getDB().message.findUnique({
|
return await getDB().message.findUnique({
|
||||||
|
include: {
|
||||||
|
reactions: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -24,7 +36,7 @@ const getMessageById = async (id: string): Promise<Message | null> => {
|
||||||
const getMessageByIdAuth = async (
|
const getMessageByIdAuth = async (
|
||||||
id: string,
|
id: string,
|
||||||
authHeader: string | undefined,
|
authHeader: string | undefined,
|
||||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
): Promise<FullMessage | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
const authUser = await getUserFromAuth(authHeader);
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
const message = await getMessageById(id);
|
const message = await getMessageById(id);
|
||||||
const channel = await getChannelById(message?.channelId ?? "");
|
const channel = await getChannelById(message?.channelId ?? "");
|
||||||
|
|
@ -50,14 +62,43 @@ const createMessage = async (
|
||||||
ownerId: string,
|
ownerId: string,
|
||||||
communityId: string,
|
communityId: string,
|
||||||
create: ICreateMessage,
|
create: ICreateMessage,
|
||||||
): Promise<Message> => {
|
): Promise<FullMessage> => {
|
||||||
const message = await getDB().message.create({
|
const message = await getDB().message.create({
|
||||||
data: {
|
data: {
|
||||||
ownerId: ownerId,
|
ownerId: ownerId,
|
||||||
...create,
|
...create,
|
||||||
|
attachments: {
|
||||||
|
connect:
|
||||||
|
create.attachments?.map((attachment) => ({
|
||||||
|
id: attachment,
|
||||||
|
})) ?? [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
reactions: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const attachmentId of create.attachments ?? []) {
|
||||||
|
await getDB().attachment.update({
|
||||||
|
where: {
|
||||||
|
id: attachmentId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
messageId: message.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const userIds =
|
const userIds =
|
||||||
await getUserIdsInCommunityReadMessagesPermission(communityId);
|
await getUserIdsInCommunityReadMessagesPermission(communityId);
|
||||||
|
|
||||||
|
|
@ -69,7 +110,10 @@ const createMessage = async (
|
||||||
id: message.id,
|
id: message.id,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
iv: message.iv ?? "",
|
iv: message.iv ?? "",
|
||||||
|
replyToId: message.replyToId ?? "",
|
||||||
edited: message.edited,
|
edited: message.edited,
|
||||||
|
reactions: [],
|
||||||
|
attachments: create.attachments ?? [],
|
||||||
ownerId: message.ownerId,
|
ownerId: message.ownerId,
|
||||||
creationDate: message.creationDate.getTime(),
|
creationDate: message.creationDate.getTime(),
|
||||||
},
|
},
|
||||||
|
|
@ -82,7 +126,7 @@ const createMessage = async (
|
||||||
const createMessageAuth = async (
|
const createMessageAuth = async (
|
||||||
create: ICreateMessage,
|
create: ICreateMessage,
|
||||||
authHeader: string | undefined,
|
authHeader: string | undefined,
|
||||||
): Promise<Message | API_ERROR.ACCESS_DENIED> => {
|
): Promise<FullMessage | API_ERROR.ACCESS_DENIED> => {
|
||||||
const authUser = await getUserFromAuth(authHeader);
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
const channel = await getChannelById(create.channelId);
|
const channel = await getChannelById(create.channelId);
|
||||||
const community = await getCommunityById(channel?.communityId ?? "");
|
const community = await getCommunityById(channel?.communityId ?? "");
|
||||||
|
|
@ -109,7 +153,7 @@ const updateMessageById = async (
|
||||||
id: string,
|
id: string,
|
||||||
communityId: string,
|
communityId: string,
|
||||||
update: IUpdateMessage,
|
update: IUpdateMessage,
|
||||||
): Promise<Message | null> => {
|
): Promise<FullMessage | null> => {
|
||||||
const message = await getMessageById(id);
|
const message = await getMessageById(id);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -126,6 +170,18 @@ const updateMessageById = async (
|
||||||
editHistory: newEditHistory,
|
editHistory: newEditHistory,
|
||||||
edited: true,
|
edited: true,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
reactions: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const userIds =
|
const userIds =
|
||||||
|
|
@ -139,7 +195,14 @@ const updateMessageById = async (
|
||||||
id: updatedMessage.id,
|
id: updatedMessage.id,
|
||||||
text: updatedMessage.text,
|
text: updatedMessage.text,
|
||||||
iv: updatedMessage.iv ?? "",
|
iv: updatedMessage.iv ?? "",
|
||||||
|
replyToId: updatedMessage.replyToId ?? "",
|
||||||
edited: updatedMessage.edited,
|
edited: updatedMessage.edited,
|
||||||
|
reactions: updatedMessage.reactions.map(
|
||||||
|
(reaction) => reaction.id,
|
||||||
|
),
|
||||||
|
attachments: updatedMessage.attachments.map(
|
||||||
|
(attachment) => attachment.id,
|
||||||
|
),
|
||||||
ownerId: updatedMessage.ownerId,
|
ownerId: updatedMessage.ownerId,
|
||||||
creationDate: updatedMessage.creationDate.getTime(),
|
creationDate: updatedMessage.creationDate.getTime(),
|
||||||
},
|
},
|
||||||
|
|
@ -153,7 +216,7 @@ const updateMessageByIdAuth = async (
|
||||||
id: string,
|
id: string,
|
||||||
update: IUpdateMessage,
|
update: IUpdateMessage,
|
||||||
authHeader: string | undefined,
|
authHeader: string | undefined,
|
||||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
): Promise<FullMessage | null | API_ERROR.ACCESS_DENIED> => {
|
||||||
const authUser = await getUserFromAuth(authHeader);
|
const authUser = await getUserFromAuth(authHeader);
|
||||||
const message = await getMessageById(id);
|
const message = await getMessageById(id);
|
||||||
const channel = await getChannelById(message?.channelId ?? "");
|
const channel = await getChannelById(message?.channelId ?? "");
|
||||||
|
|
@ -169,7 +232,7 @@ const updateMessageByIdAuth = async (
|
||||||
return API_ERROR.ACCESS_DENIED;
|
return API_ERROR.ACCESS_DENIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateMessageById(id, community?.id, update);
|
return await updateMessageById(id, community.id, update);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMessageById = async (
|
const deleteMessageById = async (
|
||||||
|
|
@ -178,8 +241,23 @@ const deleteMessageById = async (
|
||||||
): Promise<Message | null> => {
|
): Promise<Message | null> => {
|
||||||
const deletedMessage = await getDB().message.delete({
|
const deletedMessage = await getDB().message.delete({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
|
include: {
|
||||||
|
attachments: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const attachmentId of deletedMessage.attachments) {
|
||||||
|
await getDB().attachment.delete({
|
||||||
|
where: {
|
||||||
|
id: attachmentId.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const userIds =
|
const userIds =
|
||||||
await getUserIdsInCommunityReadMessagesPermission(communityId);
|
await getUserIdsInCommunityReadMessagesPermission(communityId);
|
||||||
|
|
||||||
|
|
@ -220,6 +298,237 @@ const deleteMessageByIdAuth = async (
|
||||||
return await deleteMessageById(id, community.id);
|
return await deleteMessageById(id, community.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reactMessageById = async (
|
||||||
|
id: string,
|
||||||
|
communityId: string,
|
||||||
|
userId: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<FullMessage | null> => {
|
||||||
|
const message = await getMessageById(id);
|
||||||
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userExists =
|
||||||
|
(await getDB().user.count({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
})) < 1;
|
||||||
|
if (!userExists) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reaction = await getDB().reaction.findFirst({
|
||||||
|
where: {
|
||||||
|
messageId: message.id,
|
||||||
|
content: content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!reaction) {
|
||||||
|
reaction = await getDB().reaction.create({
|
||||||
|
data: {
|
||||||
|
content: content,
|
||||||
|
messageId: message.id,
|
||||||
|
users: {
|
||||||
|
connect: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMessage = await getDB().message.update({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
reactions: {
|
||||||
|
connect: {
|
||||||
|
id: reaction.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
reactions: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
select: {
|
||||||
|
id: 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 ?? "",
|
||||||
|
replyToId: updatedMessage.replyToId ?? "",
|
||||||
|
edited: updatedMessage.edited,
|
||||||
|
reactions: updatedMessage.reactions.map(
|
||||||
|
(reaction) => reaction.id,
|
||||||
|
),
|
||||||
|
attachments: updatedMessage.attachments.map(
|
||||||
|
(attachment) => attachment.id,
|
||||||
|
),
|
||||||
|
ownerId: updatedMessage.ownerId,
|
||||||
|
creationDate: updatedMessage.creationDate.getTime(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reactMessageByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
content: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<FullMessage | 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 (
|
||||||
|
!authUser ||
|
||||||
|
!community ||
|
||||||
|
!(await isUserOwnerOrAdmin(authUser, {
|
||||||
|
message: message,
|
||||||
|
})) ||
|
||||||
|
!(await isUserInCommunity(authUser, community))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await reactMessageById(id, community.id, authUser.id, content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreactMessageById = async (
|
||||||
|
id: string,
|
||||||
|
communityId: string,
|
||||||
|
userId: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<FullMessage | null> => {
|
||||||
|
const message = await getMessageById(id);
|
||||||
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userExists =
|
||||||
|
(await getDB().user.count({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
})) < 1;
|
||||||
|
if (!userExists) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reaction = await getDB().reaction.findFirst({
|
||||||
|
where: {
|
||||||
|
messageId: message.id,
|
||||||
|
content: content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!reaction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
reaction = await getDB().reaction.delete({
|
||||||
|
where: {
|
||||||
|
id: reaction?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedMessage = await getDB().message.update({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
reactions: {
|
||||||
|
disconnect: {
|
||||||
|
id: reaction.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
reactions: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
select: {
|
||||||
|
id: 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 ?? "",
|
||||||
|
replyToId: updatedMessage.replyToId ?? "",
|
||||||
|
edited: updatedMessage.edited,
|
||||||
|
reactions: updatedMessage.reactions.map(
|
||||||
|
(reaction) => reaction.id,
|
||||||
|
),
|
||||||
|
attachments: updatedMessage.attachments.map(
|
||||||
|
(attachment) => attachment.id,
|
||||||
|
),
|
||||||
|
ownerId: updatedMessage.ownerId,
|
||||||
|
creationDate: updatedMessage.creationDate.getTime(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreactMessageByIdAuth = async (
|
||||||
|
id: string,
|
||||||
|
content: string,
|
||||||
|
authHeader: string | undefined,
|
||||||
|
): Promise<FullMessage | 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 (
|
||||||
|
!authUser ||
|
||||||
|
!community ||
|
||||||
|
!(await isUserOwnerOrAdmin(authUser, {
|
||||||
|
message: message,
|
||||||
|
})) ||
|
||||||
|
!(await isUserInCommunity(authUser, community))
|
||||||
|
) {
|
||||||
|
return API_ERROR.ACCESS_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await unreactMessageById(id, community?.id, authUser.id, content);
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getMessageById,
|
getMessageById,
|
||||||
getMessageByIdAuth,
|
getMessageByIdAuth,
|
||||||
|
|
@ -229,4 +538,8 @@ export {
|
||||||
updateMessageByIdAuth,
|
updateMessageByIdAuth,
|
||||||
deleteMessageById,
|
deleteMessageById,
|
||||||
deleteMessageByIdAuth,
|
deleteMessageByIdAuth,
|
||||||
|
reactMessageById,
|
||||||
|
reactMessageByIdAuth,
|
||||||
|
unreactMessageById,
|
||||||
|
unreactMessageByIdAuth,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
|
import type { Message } from "../../generated/prisma/client.js";
|
||||||
|
|
||||||
|
interface FullMessage extends Message {
|
||||||
|
reactions: {
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
attachments: {
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ICreateMessage {
|
interface ICreateMessage {
|
||||||
text: string;
|
text: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
replyToId?: string;
|
||||||
|
attachments?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUpdateMessage {
|
interface IUpdateMessage {
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type ICreateMessage, type IUpdateMessage };
|
export { type FullMessage, type ICreateMessage, type IUpdateMessage };
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ interface ICreateUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUpdateUser {
|
interface IUpdateUser {
|
||||||
|
nickname?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue