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
|
||||
files
|
||||
|
|
|
|||
84
package-lock.json
generated
84
package-lock.json
generated
|
|
@ -1,22 +1,24 @@
|
|||
{
|
||||
"name": "tether",
|
||||
"version": "0.5.2",
|
||||
"version": "0.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tether",
|
||||
"version": "0.5.2",
|
||||
"version": "0.6.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/multipart": "^9.3.0",
|
||||
"@fastify/websocket": "^11.2.0",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"argon2": "^0.44.0",
|
||||
"fastify": "^5.6.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mime-types": "^3.0.2",
|
||||
"pg": "^8.16.3",
|
||||
"ua-parser-js": "^2.0.8",
|
||||
"uuid": "^13.0.0",
|
||||
|
|
@ -24,6 +26,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
|
@ -140,6 +143,12 @@
|
|||
"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": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz",
|
||||
|
|
@ -180,6 +189,22 @@
|
|||
"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": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
|
||||
|
|
@ -250,6 +275,29 @@
|
|||
"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": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
|
||||
|
|
@ -591,6 +639,13 @@
|
|||
"@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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
|
|
@ -1575,6 +1630,31 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tether",
|
||||
"version": "0.5.2",
|
||||
"version": "0.6.0",
|
||||
"description": "Communication server using the Nexlink protocol",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
|
|
@ -28,12 +29,14 @@
|
|||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/multipart": "^9.3.0",
|
||||
"@fastify/websocket": "^11.2.0",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"argon2": "^0.44.0",
|
||||
"fastify": "^5.6.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mime-types": "^3.0.2",
|
||||
"pg": "^8.16.3",
|
||||
"ua-parser-js": "^2.0.8",
|
||||
"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;
|
||||
|
|
@ -9,16 +9,19 @@ datasource db {
|
|||
}
|
||||
|
||||
model Community {
|
||||
id String @id @unique @default(uuid())
|
||||
name String @unique
|
||||
id String @id @unique @default(uuid())
|
||||
name String @unique
|
||||
description String?
|
||||
creationDate DateTime @default(now())
|
||||
User User @relation(name: "OwnerCommunityToUser", fields: [ownerId], references: [id])
|
||||
avatar String?
|
||||
creationDate DateTime @default(now())
|
||||
User User @relation(name: "OwnerCommunityToUser", fields: [ownerId], references: [id])
|
||||
ownerId String
|
||||
members User[] @relation(name: "MembersCommunitiesToUsers")
|
||||
members User[] @relation(name: "MembersCommunitiesToUsers")
|
||||
channels Channel[]
|
||||
roles Role[]
|
||||
invites Invite[]
|
||||
attachments Attachment[]
|
||||
chunks Chunk[]
|
||||
}
|
||||
|
||||
model Channel {
|
||||
|
|
@ -45,9 +48,11 @@ model Role {
|
|||
model User {
|
||||
id String @id @unique @default(uuid())
|
||||
username String @unique
|
||||
nickname String?
|
||||
email String? @unique
|
||||
passwordHash String?
|
||||
description String?
|
||||
avatar String?
|
||||
admin Boolean @default(false)
|
||||
registerDate DateTime @default(now())
|
||||
lastLogin DateTime?
|
||||
|
|
@ -57,6 +62,7 @@ model User {
|
|||
communities Community[] @relation(name: "MembersCommunitiesToUsers")
|
||||
roles Role[] @relation(name: "UsersRolesToUsers")
|
||||
messages Message[]
|
||||
reactions Reaction[]
|
||||
}
|
||||
|
||||
model Session {
|
||||
|
|
@ -84,14 +90,49 @@ model Invite {
|
|||
}
|
||||
|
||||
model Message {
|
||||
id String @id @unique @default(uuid())
|
||||
id String @id @unique @default(uuid())
|
||||
text String
|
||||
iv String?
|
||||
editHistory String[] @default([])
|
||||
edited Boolean @default(false)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
replyToId String?
|
||||
edited Boolean @default(false)
|
||||
editHistory String[] @default([])
|
||||
reactions Reaction[]
|
||||
attachments Attachment[]
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
ownerId String
|
||||
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||
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,
|
||||
} from "../../services/channel/channel.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
import { responseFullChannel } from "./helpers.js";
|
||||
|
||||
const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetChannelParams;
|
||||
|
|
@ -46,13 +47,7 @@ const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IGetChannelResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
description: channel.description,
|
||||
communityId: channel.communityId,
|
||||
creationDate: channel.creationDate.getTime(),
|
||||
} as IGetChannelResponseSuccess;
|
||||
return responseFullChannel(channel) as IGetChannelResponseSuccess;
|
||||
};
|
||||
|
||||
const postCreateChannel = async (
|
||||
|
|
@ -70,13 +65,7 @@ const postCreateChannel = async (
|
|||
} as IPostCreateChannelResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
description: channel.description,
|
||||
communityId: channel.communityId,
|
||||
creationDate: channel.creationDate.getTime(),
|
||||
} as IPostCreateChannelResponseSuccess;
|
||||
return responseFullChannel(channel) as IPostCreateChannelResponseSuccess;
|
||||
};
|
||||
|
||||
const patchChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -104,13 +93,7 @@ const patchChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IPatchChannelResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
description: channel.description,
|
||||
communityId: channel.communityId,
|
||||
creationDate: channel.creationDate.getTime(),
|
||||
} as IPatchChannelResponseSuccess;
|
||||
return responseFullChannel(channel) as IPatchChannelResponseSuccess;
|
||||
};
|
||||
|
||||
const deleteChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -165,7 +148,10 @@ const getMessages = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
id: message.id,
|
||||
text: message.text,
|
||||
iv: message.iv,
|
||||
replyToId: message.replyToId,
|
||||
edited: message.edited,
|
||||
reactions: message.reactions.map((reaction) => reaction.id),
|
||||
attachments: message.attachments.map((attachment) => attachment.id),
|
||||
ownerId: message.ownerId,
|
||||
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 "./routes.js";
|
||||
export * from "./types.js";
|
||||
export * from "./helpers.js";
|
||||
|
|
|
|||
|
|
@ -80,7 +80,10 @@ interface IGetMessagesResponseMessage {
|
|||
id: string;
|
||||
text: string;
|
||||
iv: string;
|
||||
replyToId?: string;
|
||||
edited: boolean;
|
||||
reactions: string[];
|
||||
attachments: string[];
|
||||
ownerId: string;
|
||||
creationDate: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
} from "../../services/community/community.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
import type { ICreateInvite } from "../../services/community/types.js";
|
||||
import { responseFullCommunity } from "./helpers.js";
|
||||
|
||||
const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetCommunityParams;
|
||||
|
|
@ -60,13 +61,7 @@ const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IGetCommunityResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: community.id,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
ownerId: community.ownerId,
|
||||
creationDate: community.creationDate.getTime(),
|
||||
} as IGetCommunityResponseSuccess;
|
||||
return responseFullCommunity(community) as IGetCommunityResponseSuccess;
|
||||
};
|
||||
|
||||
const postCreateCommunity = async (
|
||||
|
|
@ -87,13 +82,9 @@ const postCreateCommunity = async (
|
|||
} as IPostCreateCommunityResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: community.id,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
ownerId: community.ownerId,
|
||||
creationDate: community.creationDate.getTime(),
|
||||
} as IPostCreateCommunityResponseSuccess;
|
||||
return responseFullCommunity(
|
||||
community,
|
||||
) as IPostCreateCommunityResponseSuccess;
|
||||
};
|
||||
|
||||
const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -121,13 +112,7 @@ const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IPatchCommunityResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: community.id,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
ownerId: community.ownerId,
|
||||
creationDate: community.creationDate.getTime(),
|
||||
} as IPatchCommunityResponseSuccess;
|
||||
return responseFullCommunity(community) as IPatchCommunityResponseSuccess;
|
||||
};
|
||||
|
||||
const deleteCommunity = async (
|
||||
|
|
@ -185,6 +170,8 @@ const getMembers = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
members: members.map((member) => ({
|
||||
id: member.id,
|
||||
username: member.username,
|
||||
nickname: member.nickname ?? member.username,
|
||||
avatar: member.avatar,
|
||||
})),
|
||||
} 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 "./routes.js";
|
||||
export * from "./types.js";
|
||||
export * from "./helpers.js";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ interface ICommunity {
|
|||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
avatar?: string;
|
||||
ownerId: string;
|
||||
creationDate: number;
|
||||
}
|
||||
|
|
@ -78,6 +79,8 @@ interface IGetMembersResponseSuccess {
|
|||
interface IGetMembersResponseMember {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface IGetChannelsParams {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
enum API_ERROR {
|
||||
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
||||
NOT_FOUND = "NOT_FOUND",
|
||||
ACCESS_DENIED = "ACCESS_DENIED",
|
||||
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
||||
FILE_INVALID = "FILE_INVALID",
|
||||
}
|
||||
|
||||
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 "./routes.js";
|
||||
export * from "./types.js";
|
||||
export * from "./helpers.js";
|
||||
|
|
|
|||
|
|
@ -13,14 +13,25 @@ import type {
|
|||
IDeleteMessageParams,
|
||||
IDeleteMessageResponseError,
|
||||
IDeleteMessageResponseSuccess,
|
||||
IReactMessageParams,
|
||||
IReactMessageRequest,
|
||||
IReactMessageResponseError,
|
||||
IReactMessageResponseSuccess,
|
||||
IUnreactMessageParams,
|
||||
IUnreactMessageRequest,
|
||||
IUnreactMessageResponseError,
|
||||
IUnreactMessageResponseSuccess,
|
||||
} from "./types.js";
|
||||
import {
|
||||
createMessageAuth,
|
||||
deleteMessageByIdAuth,
|
||||
getMessageByIdAuth,
|
||||
updateMessageByIdAuth,
|
||||
reactMessageByIdAuth,
|
||||
unreactMessageByIdAuth,
|
||||
} from "../../services/message/message.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
import { responseFullMessage } from "./helpers.js";
|
||||
|
||||
const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetMessageParams;
|
||||
|
|
@ -42,16 +53,7 @@ const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IGetMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
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;
|
||||
return responseFullMessage(message) as IGetMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const postCreateMessage = async (
|
||||
|
|
@ -69,16 +71,7 @@ const postCreateMessage = async (
|
|||
} as IPostCreateMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
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;
|
||||
return responseFullMessage(message) as IPostCreateMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const patchMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -106,15 +99,7 @@ const patchMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IPatchMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
editHistory: message.editHistory,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
} as IPatchMessageResponseSuccess;
|
||||
return responseFullMessage(message) as IPatchMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -144,4 +129,59 @@ const deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} 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.patch(`/:id`, controller.patchMessage);
|
||||
fastify.delete(`/:id`, controller.deleteMessage);
|
||||
fastify.patch(`/:id/react`, controller.reactMessage);
|
||||
fastify.patch(`/:id/unreact`, controller.unreactMessage);
|
||||
};
|
||||
|
||||
export { messageRoutes };
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ interface IMessage {
|
|||
id: string;
|
||||
text: string;
|
||||
iv: string;
|
||||
editHistory: string[];
|
||||
replyToId?: string;
|
||||
edited: boolean;
|
||||
editHistory: string[];
|
||||
reactions: string[];
|
||||
attachments: string[];
|
||||
ownerId: string;
|
||||
channelId: string;
|
||||
creationDate: number;
|
||||
|
|
@ -26,6 +29,8 @@ interface IPostCreateMessageRequest {
|
|||
text: string;
|
||||
iv: string;
|
||||
channelId: string;
|
||||
replyToId?: string;
|
||||
attachments: string[];
|
||||
}
|
||||
|
||||
interface IPostCreateMessageResponseError {
|
||||
|
|
@ -65,6 +70,36 @@ interface IDeleteMessageResponseSuccess {
|
|||
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 {
|
||||
type IMessage,
|
||||
type IGetMessageParams,
|
||||
|
|
@ -80,4 +115,12 @@ export {
|
|||
type IDeleteMessageParams,
|
||||
type IDeleteMessageResponseError,
|
||||
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 "./routes.js";
|
||||
export * from "./types.js";
|
||||
export * from "./helpers.js";
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
updateRoleByIdAuth,
|
||||
} from "../../services/role/role.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
import { responseFullRole } from "./helpers.js";
|
||||
|
||||
const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetRoleParams;
|
||||
|
|
@ -52,14 +53,7 @@ const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IGetRoleResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
communityId: role.communityId,
|
||||
permissions: role.permissions,
|
||||
creationDate: role.creationDate.getTime(),
|
||||
} as IGetRoleResponseSuccess;
|
||||
return responseFullRole(role) as IGetRoleResponseSuccess;
|
||||
};
|
||||
|
||||
const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -74,14 +68,7 @@ const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IPostCreateRoleResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
communityId: role.communityId,
|
||||
permissions: role.permissions,
|
||||
creationDate: role.creationDate.getTime(),
|
||||
} as IPostCreateRoleResponseSuccess;
|
||||
return responseFullRole(role) as IPostCreateRoleResponseSuccess;
|
||||
};
|
||||
|
||||
const patchRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -105,14 +92,7 @@ const patchRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IPatchRoleResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
communityId: role.communityId,
|
||||
permissions: role.permissions,
|
||||
creationDate: role.creationDate.getTime(),
|
||||
} as IPatchRoleResponseSuccess;
|
||||
return responseFullRole(role) as IPatchRoleResponseSuccess;
|
||||
};
|
||||
|
||||
const deleteRole = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ interface IRole {
|
|||
name: string;
|
||||
description: string;
|
||||
communityId: string;
|
||||
permissions: PERMISSION[];
|
||||
permissions: string[];
|
||||
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 "./routes.js";
|
||||
export * from "./types.js";
|
||||
export * from "./helpers.js";
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import type { API_ERROR } from "../errors.js";
|
|||
interface IUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
description: string;
|
||||
nickname?: string;
|
||||
email?: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
admin: boolean;
|
||||
registerDate: number;
|
||||
lastLogin: number;
|
||||
|
|
@ -49,6 +51,7 @@ interface IPatchUserParams {
|
|||
}
|
||||
|
||||
interface IPatchUserRequest {
|
||||
nickname?: string;
|
||||
email?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
|
@ -114,6 +117,7 @@ interface IGetCommunitiesResponseCommunity {
|
|||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
getLoggedUserAuth,
|
||||
} from "../../services/user/user.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
import { responseFullUser } from "./helpers.js";
|
||||
|
||||
const getUserLogged = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
|
@ -69,15 +70,7 @@ const getUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IGetUserResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
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;
|
||||
return responseFullUser(user) as IGetUserResponseSuccess;
|
||||
};
|
||||
|
||||
const postCreateUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -92,15 +85,7 @@ const postCreateUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IPostCreateUserResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
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;
|
||||
return responseFullUser(user) as IPostCreateUserResponseSuccess;
|
||||
};
|
||||
|
||||
const patchUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -124,15 +109,7 @@ const patchUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IPatchUserResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
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;
|
||||
return responseFullUser(user) as IPatchUserResponseSuccess;
|
||||
};
|
||||
|
||||
const deleteUser = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
|
|
@ -219,6 +196,7 @@ const getCommunities = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
id: community.id,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
avatar: community.avatar,
|
||||
})),
|
||||
} as IGetCommunitiesResponseSuccess;
|
||||
};
|
||||
|
|
|
|||
16
src/index.ts
16
src/index.ts
|
|
@ -2,6 +2,7 @@ import Fastify from "fastify";
|
|||
import cors from "@fastify/cors";
|
||||
import cookie from "@fastify/cookie";
|
||||
import websocket from "@fastify/websocket";
|
||||
import multipart from "@fastify/multipart";
|
||||
|
||||
import { config } from "./config.js";
|
||||
|
||||
|
|
@ -16,7 +17,9 @@ import { channelRoutes } from "./controllers/channel/routes.js";
|
|||
import { roleRoutes } from "./controllers/role/routes.js";
|
||||
import { inviteRoutes } from "./controllers/invite/routes.js";
|
||||
import { messageRoutes } from "./controllers/message/routes.js";
|
||||
import { fileRoutes } from "./controllers/file/routes.js";
|
||||
import { websocketRoutes } from "./controllers/websocket/routes.js";
|
||||
|
||||
import { initializeCommunitiesWithReadableUsersCache } from "./services/user/user.js";
|
||||
|
||||
const app = Fastify({
|
||||
|
|
@ -28,10 +31,18 @@ app.register(cors, {
|
|||
credentials: true,
|
||||
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
||||
});
|
||||
|
||||
app.register(cookie, { secret: getCookieSecret() });
|
||||
|
||||
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(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(inviteRoutes, { prefix: "/api/v1/invite" });
|
||||
app.register(messageRoutes, { prefix: "/api/v1/message" });
|
||||
app.register(fileRoutes, { prefix: "/api/v1/file" });
|
||||
app.register(websocketRoutes, { prefix: "/ws" });
|
||||
|
||||
app.listen({ port: config.port }, (err, address) => {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const registerUser = async (
|
|||
newUser = await getDB().user.create({
|
||||
data: {
|
||||
username: registration.username,
|
||||
nickname: registration.username,
|
||||
passwordHash: passwordHash,
|
||||
email: registration.email ?? null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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 { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
||||
import { PERMISSION } from "../auth/permission.js";
|
||||
import { getCommunityById } from "../community/community.js";
|
||||
import type { FullMessage } from "../message/types.js";
|
||||
import { getUserIdsInCommunity } from "../user/user.js";
|
||||
import { SocketMessageTypes } from "../websocket/types.js";
|
||||
import { sendMessageToUsersWS } from "../websocket/websocket.js";
|
||||
|
|
@ -174,8 +175,20 @@ const deleteChannelByIdAuth = async (
|
|||
|
||||
const getChannelMessagesById = async (
|
||||
id: string,
|
||||
): Promise<Message[] | null> => {
|
||||
): Promise<FullMessage[] | null> => {
|
||||
return await getDB().message.findMany({
|
||||
include: {
|
||||
reactions: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
attachments: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
channelId: id,
|
||||
},
|
||||
|
|
@ -185,7 +198,7 @@ const getChannelMessagesById = async (
|
|||
const getChannelMessagesByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message[] | null | API_ERROR.ACCESS_DENIED> => {
|
||||
): Promise<FullMessage[] | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const channel = await getChannelById(id);
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ const getCommunityMembersById = async (
|
|||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nickname: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ interface IUpdateCommunity {
|
|||
interface ICommunityMember {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname?: string | null;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
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 { SocketMessageTypes } from "../websocket/types.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({
|
||||
include: {
|
||||
reactions: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
attachments: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: { id: id },
|
||||
});
|
||||
};
|
||||
|
|
@ -24,7 +36,7 @@ const getMessageById = async (id: string): Promise<Message | null> => {
|
|||
const getMessageByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
): Promise<FullMessage | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
|
|
@ -50,14 +62,43 @@ const createMessage = async (
|
|||
ownerId: string,
|
||||
communityId: string,
|
||||
create: ICreateMessage,
|
||||
): Promise<Message> => {
|
||||
): Promise<FullMessage> => {
|
||||
const message = await getDB().message.create({
|
||||
data: {
|
||||
ownerId: ownerId,
|
||||
...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 =
|
||||
await getUserIdsInCommunityReadMessagesPermission(communityId);
|
||||
|
||||
|
|
@ -69,7 +110,10 @@ const createMessage = async (
|
|||
id: message.id,
|
||||
text: message.text,
|
||||
iv: message.iv ?? "",
|
||||
replyToId: message.replyToId ?? "",
|
||||
edited: message.edited,
|
||||
reactions: [],
|
||||
attachments: create.attachments ?? [],
|
||||
ownerId: message.ownerId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
},
|
||||
|
|
@ -82,7 +126,7 @@ const createMessage = async (
|
|||
const createMessageAuth = async (
|
||||
create: ICreateMessage,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | API_ERROR.ACCESS_DENIED> => {
|
||||
): Promise<FullMessage | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const channel = await getChannelById(create.channelId);
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
|
@ -109,7 +153,7 @@ const updateMessageById = async (
|
|||
id: string,
|
||||
communityId: string,
|
||||
update: IUpdateMessage,
|
||||
): Promise<Message | null> => {
|
||||
): Promise<FullMessage | null> => {
|
||||
const message = await getMessageById(id);
|
||||
if (!message) {
|
||||
return null;
|
||||
|
|
@ -126,6 +170,18 @@ const updateMessageById = async (
|
|||
editHistory: newEditHistory,
|
||||
edited: true,
|
||||
},
|
||||
include: {
|
||||
reactions: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
attachments: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userIds =
|
||||
|
|
@ -139,7 +195,14 @@ const updateMessageById = async (
|
|||
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(),
|
||||
},
|
||||
|
|
@ -153,7 +216,7 @@ const updateMessageByIdAuth = async (
|
|||
id: string,
|
||||
update: IUpdateMessage,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
): Promise<FullMessage | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
|
|
@ -169,7 +232,7 @@ const updateMessageByIdAuth = async (
|
|||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await updateMessageById(id, community?.id, update);
|
||||
return await updateMessageById(id, community.id, update);
|
||||
};
|
||||
|
||||
const deleteMessageById = async (
|
||||
|
|
@ -178,8 +241,23 @@ const deleteMessageById = async (
|
|||
): Promise<Message | null> => {
|
||||
const deletedMessage = await getDB().message.delete({
|
||||
where: { id: id },
|
||||
include: {
|
||||
attachments: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const attachmentId of deletedMessage.attachments) {
|
||||
await getDB().attachment.delete({
|
||||
where: {
|
||||
id: attachmentId.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const userIds =
|
||||
await getUserIdsInCommunityReadMessagesPermission(communityId);
|
||||
|
||||
|
|
@ -220,6 +298,237 @@ const deleteMessageByIdAuth = async (
|
|||
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 {
|
||||
getMessageById,
|
||||
getMessageByIdAuth,
|
||||
|
|
@ -229,4 +538,8 @@ export {
|
|||
updateMessageByIdAuth,
|
||||
deleteMessageById,
|
||||
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 {
|
||||
text: string;
|
||||
iv: string;
|
||||
channelId: string;
|
||||
replyToId?: string;
|
||||
attachments?: string[];
|
||||
}
|
||||
|
||||
interface IUpdateMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export { type ICreateMessage, type IUpdateMessage };
|
||||
export { type FullMessage, type ICreateMessage, type IUpdateMessage };
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface ICreateUser {
|
|||
}
|
||||
|
||||
interface IUpdateUser {
|
||||
nickname?: string;
|
||||
email?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue