From 8d3b0fa7d350ff2697ccc9e963e763b269d73893170b0955bc4a04f71cb71e91 Mon Sep 17 00:00:00 2001 From: aslan Date: Tue, 20 Jan 2026 14:08:51 -0500 Subject: [PATCH] Version 0.7.0 --- package.json | 2 +- .../migration.sql | 2 + .../migration.sql | 7 + .../20260120164052_roles_show/migration.sql | 2 + .../20260120183150_order/migration.sql | 9 ++ prisma/schema.prisma | 24 +-- src/controllers/announcement/announcement.ts | 35 +++++ src/controllers/announcement/index.ts | 3 + src/controllers/announcement/routes.ts | 8 + src/controllers/announcement/types.ts | 12 ++ src/controllers/channel/helpers.ts | 2 + src/controllers/channel/types.ts | 3 + src/controllers/community/community.ts | 78 +++++++++ src/controllers/community/routes.ts | 2 + src/controllers/community/types.ts | 56 +++++++ src/controllers/file/file.ts | 3 +- src/controllers/file/helpers.ts | 1 + src/controllers/file/types.ts | 2 + src/controllers/message/types.ts | 1 + src/controllers/role/helpers.ts | 3 + src/controllers/role/role.ts | 12 ++ src/controllers/role/routes.ts | 1 + src/controllers/role/types.ts | 11 ++ src/controllers/user/routes.ts | 4 + src/controllers/user/types.ts | 26 +++ src/controllers/user/user.ts | 43 +++++ src/index.ts | 2 + src/services/announcement/announcement.ts | 30 ++++ src/services/announcement/index.ts | 2 + src/services/announcement/types.ts | 6 + src/services/auth/auth.ts | 3 +- src/services/auth/helpers.ts | 5 + src/services/channel/channel.ts | 1 + src/services/channel/types.ts | 3 + src/services/community/community.ts | 148 ++++++++++++++++-- src/services/community/types.ts | 13 +- src/services/file/types.ts | 1 + src/services/invite/invite.ts | 4 + src/services/message/message.ts | 8 - src/services/message/types.ts | 1 + src/services/role/types.ts | 4 + src/services/user/user.ts | 51 +++++- src/services/websocket/types.ts | 9 +- src/services/websocket/websocket.ts | 9 ++ 44 files changed, 611 insertions(+), 41 deletions(-) create mode 100644 prisma/migrations/20260118193129_attachment_iv/migration.sql create mode 100644 prisma/migrations/20260120105728_order_categories/migration.sql create mode 100644 prisma/migrations/20260120164052_roles_show/migration.sql create mode 100644 prisma/migrations/20260120183150_order/migration.sql create mode 100644 src/controllers/announcement/announcement.ts create mode 100644 src/controllers/announcement/index.ts create mode 100644 src/controllers/announcement/routes.ts create mode 100644 src/controllers/announcement/types.ts create mode 100644 src/services/announcement/announcement.ts create mode 100644 src/services/announcement/index.ts create mode 100644 src/services/announcement/types.ts diff --git a/package.json b/package.json index aa9e191..667b0b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tether", - "version": "0.6.0", + "version": "0.7.0", "description": "Communication server using the Nexlink protocol", "repository": { "type": "git", diff --git a/prisma/migrations/20260118193129_attachment_iv/migration.sql b/prisma/migrations/20260118193129_attachment_iv/migration.sql new file mode 100644 index 0000000..6871d05 --- /dev/null +++ b/prisma/migrations/20260118193129_attachment_iv/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Attachment" ADD COLUMN "iv" TEXT; diff --git a/prisma/migrations/20260120105728_order_categories/migration.sql b/prisma/migrations/20260120105728_order_categories/migration.sql new file mode 100644 index 0000000..c63929c --- /dev/null +++ b/prisma/migrations/20260120105728_order_categories/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "Channel" ADD COLUMN "category" TEXT, +ADD COLUMN "order" INTEGER; + +-- AlterTable +ALTER TABLE "Role" ADD COLUMN "color" TEXT, +ADD COLUMN "order" INTEGER; diff --git a/prisma/migrations/20260120164052_roles_show/migration.sql b/prisma/migrations/20260120164052_roles_show/migration.sql new file mode 100644 index 0000000..6b2d0b2 --- /dev/null +++ b/prisma/migrations/20260120164052_roles_show/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Role" ADD COLUMN "showInMembers" BOOLEAN; diff --git a/prisma/migrations/20260120183150_order/migration.sql b/prisma/migrations/20260120183150_order/migration.sql new file mode 100644 index 0000000..48bec21 --- /dev/null +++ b/prisma/migrations/20260120183150_order/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +CREATE SEQUENCE channel_order_seq; +ALTER TABLE "Channel" ALTER COLUMN "order" SET DEFAULT nextval('channel_order_seq'); +ALTER SEQUENCE channel_order_seq OWNED BY "Channel"."order"; + +-- AlterTable +CREATE SEQUENCE role_order_seq; +ALTER TABLE "Role" ALTER COLUMN "order" SET DEFAULT nextval('role_order_seq'); +ALTER SEQUENCE role_order_seq OWNED BY "Role"."order"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d8751a8..6ae7ea2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,21 +28,26 @@ model Channel { id String @id @unique @default(uuid()) name String description String? + category String? + order Int? @default(autoincrement()) community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) communityId String - creationDate DateTime @default(now()) messages Message[] + creationDate DateTime @default(now()) } model Role { - id String @id @unique @default(uuid()) - name String - description String? - community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) - communityId String - users User[] @relation(name: "UsersRolesToUsers") - permissions String[] - creationDate DateTime @default(now()) + id String @id @unique @default(uuid()) + name String + description String? + color String? + order Int? @default(autoincrement()) + showInMembers Boolean? + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) + communityId String + users User[] @relation(name: "UsersRolesToUsers") + permissions String[] + creationDate DateTime @default(now()) } model User { @@ -115,6 +120,7 @@ model Reaction { model Attachment { id String @id @unique @default(uuid()) + iv String? filename String mimetype String size BigInt diff --git a/src/controllers/announcement/announcement.ts b/src/controllers/announcement/announcement.ts new file mode 100644 index 0000000..e98f8dc --- /dev/null +++ b/src/controllers/announcement/announcement.ts @@ -0,0 +1,35 @@ +import { type FastifyReply, type FastifyRequest } from "fastify"; +import type { + IPostAnnouncementRequest, + IPostAnnouncementError, +} from "./types.js"; +import { API_ERROR } from "../errors.js"; +import { createAnnouncementAuth } from "../../services/announcement/announcement.js"; + +const postAnnouncement = async ( + request: FastifyRequest, + reply: FastifyReply, +) => { + const { title, text } = request.body as IPostAnnouncementRequest; + const authHeader = request.headers["authorization"]; + + const result = await createAnnouncementAuth( + { + title: title, + text: text, + }, + authHeader, + ); + + if (result === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + error: API_ERROR.ACCESS_DENIED, + } as IPostAnnouncementError; + } + + reply.status(200); + return; +}; + +export { postAnnouncement }; diff --git a/src/controllers/announcement/index.ts b/src/controllers/announcement/index.ts new file mode 100644 index 0000000..cc6479b --- /dev/null +++ b/src/controllers/announcement/index.ts @@ -0,0 +1,3 @@ +export * from "./announcement.js"; +export * from "./routes.js"; +export * from "./types.js"; diff --git a/src/controllers/announcement/routes.ts b/src/controllers/announcement/routes.ts new file mode 100644 index 0000000..ac0fdd7 --- /dev/null +++ b/src/controllers/announcement/routes.ts @@ -0,0 +1,8 @@ +import { type FastifyInstance } from "fastify"; +import * as controller from "./announcement.js"; + +const announcementRoutes = async (fastify: FastifyInstance) => { + fastify.post(`/`, controller.postAnnouncement); +}; + +export { announcementRoutes }; diff --git a/src/controllers/announcement/types.ts b/src/controllers/announcement/types.ts new file mode 100644 index 0000000..5d7793b --- /dev/null +++ b/src/controllers/announcement/types.ts @@ -0,0 +1,12 @@ +import type { API_ERROR } from "../errors.js"; + +interface IPostAnnouncementRequest { + title: string; + text: string; +} + +interface IPostAnnouncementError { + error: API_ERROR; +} + +export { type IPostAnnouncementRequest, type IPostAnnouncementError }; diff --git a/src/controllers/channel/helpers.ts b/src/controllers/channel/helpers.ts index 02c7693..4cacfa9 100644 --- a/src/controllers/channel/helpers.ts +++ b/src/controllers/channel/helpers.ts @@ -6,6 +6,8 @@ const responseFullChannel = (channel: Channel) => id: channel.id, name: channel.name, description: channel.description, + category: channel.category, + order: channel.order, communityId: channel.communityId, creationDate: channel.creationDate.getTime(), }) as IChannel; diff --git a/src/controllers/channel/types.ts b/src/controllers/channel/types.ts index 49aae7d..48df560 100644 --- a/src/controllers/channel/types.ts +++ b/src/controllers/channel/types.ts @@ -4,6 +4,8 @@ interface IChannel { id: string; name: string; description: string; + category: string; + order: number; communityId: string; creationDate: number; } @@ -39,6 +41,7 @@ interface IPatchChannelParams { interface IPatchChannelRequest { name?: string; description?: string; + category?: string; } interface IPatchChannelResponseError { diff --git a/src/controllers/community/community.ts b/src/controllers/community/community.ts index bc69bc9..f705911 100644 --- a/src/controllers/community/community.ts +++ b/src/controllers/community/community.ts @@ -25,6 +25,14 @@ import type { IGetInvitesParams, IGetInvitesResponseError, IGetInvitesResponseSuccess, + IPatchCommunityChannelOrderParams, + IPatchCommunityChannelOrderRequest, + IPatchCommunityChannelOrderResponseError, + IPatchCommunityChannelOrderResponseSuccess, + IPatchCommunityRoleOrderParams, + IPatchCommunityRoleOrderRequest, + IPatchCommunityRoleOrderResponseError, + IPatchCommunityRoleOrderResponseSuccess, IPostCreateInviteParams, IPostCreateInviteRequest, IPostCreateInviteResponseError, @@ -42,12 +50,18 @@ import { getCommunityMembersByIdAuth, getCommunityRolesByIdAuth, getCommunityInvitesByIdAuth, + updateCommunityChannelOrderByIdAuth, + updateCommunityRoleOrderByIdAuth, createInviteAuth, deleteMemberByIdAuth, } from "../../services/community/community.js"; import { API_ERROR } from "../errors.js"; import type { ICreateInvite } from "../../services/community/types.js"; import { responseFullCommunity } from "./helpers.js"; +import { + hasUnlimitedInvites, + isInviteValid, +} from "../../services/invite/invite.js"; const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as IGetCommunityParams; @@ -203,6 +217,9 @@ const getChannels = async (request: FastifyRequest, reply: FastifyReply) => { channels: channels.map((channel) => ({ id: channel.id, name: channel.name, + description: channel.description, + category: channel.category, + order: channel.order, })), } as IGetChannelsResponseSuccess; }; @@ -234,6 +251,9 @@ const getRoles = async (request: FastifyRequest, reply: FastifyReply) => { roles: roles.map((role) => ({ id: role.id, name: role.name, + color: role.color, + order: role.order, + showInMembers: role.showInMembers, })), } as IGetRolesResponseSuccess; }; @@ -264,10 +284,66 @@ const getInvites = async (request: FastifyRequest, reply: FastifyReply) => { name: community.name, invites: invites.map((invite) => ({ id: invite.id, + communityId: invite.communityId, + valid: isInviteValid(invite), + unlimitedInvites: hasUnlimitedInvites(invite), + hasExpiration: invite.expirationDate != null, + totalInvites: invite.totalInvites, + remainingInvites: invite.remainingInvites, + creationDate: invite.creationDate.getTime(), + expirationDate: invite.expirationDate?.getTime() ?? 0, })), } as IGetInvitesResponseSuccess; }; +const patchCommunityChannelOrder = async ( + request: FastifyRequest, + reply: FastifyReply, +) => { + const { id } = request.params as IPatchCommunityChannelOrderParams; + const { order } = request.body as IPatchCommunityChannelOrderRequest; + const authHeader = request.headers["authorization"]; + + const result = await updateCommunityChannelOrderByIdAuth( + id, + order, + authHeader, + ); + if (result === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IPatchCommunityChannelOrderResponseError; + } + + return { id } as IPatchCommunityChannelOrderResponseSuccess; +}; + +const patchCommunityRoleOrder = async ( + request: FastifyRequest, + reply: FastifyReply, +) => { + const { id } = request.params as IPatchCommunityRoleOrderParams; + const { order } = request.body as IPatchCommunityRoleOrderRequest; + const authHeader = request.headers["authorization"]; + + const result = await updateCommunityRoleOrderByIdAuth( + id, + order, + authHeader, + ); + if (result === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IPatchCommunityRoleOrderResponseError; + } + + return { id } as IPatchCommunityRoleOrderResponseSuccess; +}; + const postCreateInvite = async ( request: FastifyRequest, reply: FastifyReply, @@ -347,6 +423,8 @@ export { getChannels, getRoles, getInvites, + patchCommunityChannelOrder, + patchCommunityRoleOrder, postCreateInvite, deleteCommunityMember, }; diff --git a/src/controllers/community/routes.ts b/src/controllers/community/routes.ts index 45fc083..65d885c 100644 --- a/src/controllers/community/routes.ts +++ b/src/controllers/community/routes.ts @@ -10,6 +10,8 @@ const communityRoutes = async (fastify: FastifyInstance) => { fastify.get(`/:id/channels`, controller.getChannels); fastify.get(`/:id/roles`, controller.getRoles); fastify.get(`/:id/invites`, controller.getInvites); + fastify.patch(`/:id/channels/order`, controller.patchCommunityChannelOrder); + fastify.patch(`/:id/roles/order`, controller.patchCommunityRoleOrder); fastify.post(`/:id/invite`, controller.postCreateInvite); fastify.delete(`/:id/members/:memberId`, controller.deleteCommunityMember); }; diff --git a/src/controllers/community/types.ts b/src/controllers/community/types.ts index 15884b3..b59df6d 100644 --- a/src/controllers/community/types.ts +++ b/src/controllers/community/types.ts @@ -101,6 +101,9 @@ interface IGetChannelsResponseSuccess { interface IGetChannelsResponseChannel { id: string; name: string; + description: string; + category: string; + order: number; } interface IGetRolesParams { @@ -121,6 +124,9 @@ interface IGetRolesResponseSuccess { interface IGetRolesResponseRole { id: string; name: string; + color: string; + order: number; + showInMembers: boolean; } interface IGetInvitesParams { @@ -140,6 +146,48 @@ interface IGetInvitesResponseSuccess { interface IGetInvitesResponseInvite { id: string; + communityId: string; + valid: boolean; + unlimitedInvites: boolean; + hasExpiration: boolean; + totalInvites: number; + remainingInvites: number; + creationDate: number; + expirationDate: number; +} + +interface IPatchCommunityChannelOrderParams { + id: string; +} + +interface IPatchCommunityChannelOrderRequest { + order: string[]; +} + +interface IPatchCommunityChannelOrderResponseError { + id: string; + error: API_ERROR; +} + +interface IPatchCommunityChannelOrderResponseSuccess { + id: string; +} + +interface IPatchCommunityRoleOrderParams { + id: string; +} + +interface IPatchCommunityRoleOrderRequest { + order: string[]; +} + +interface IPatchCommunityRoleOrderResponseError { + id: string; + error: API_ERROR; +} + +interface IPatchCommunityRoleOrderResponseSuccess { + id: string; } interface IPostCreateInviteParams { @@ -207,6 +255,14 @@ export { type IGetInvitesResponseError, type IGetInvitesResponseSuccess, type IGetInvitesResponseInvite, + type IPatchCommunityChannelOrderParams, + type IPatchCommunityChannelOrderRequest, + type IPatchCommunityChannelOrderResponseError, + type IPatchCommunityChannelOrderResponseSuccess, + type IPatchCommunityRoleOrderParams, + type IPatchCommunityRoleOrderRequest, + type IPatchCommunityRoleOrderResponseError, + type IPatchCommunityRoleOrderResponseSuccess, type IPostCreateInviteParams, type IPostCreateInviteRequest, type IPostCreateInviteResponseError, diff --git a/src/controllers/file/file.ts b/src/controllers/file/file.ts index 77bce0c..d1f0e21 100644 --- a/src/controllers/file/file.ts +++ b/src/controllers/file/file.ts @@ -216,12 +216,13 @@ const postCreateAttachment = async ( request: FastifyRequest, reply: FastifyReply, ) => { - const { filename, mimetype, size, communityId } = + const { iv, filename, mimetype, size, communityId } = request.body as IPostCreateAttachmentRequest; const authHeader = request.headers["authorization"]; const attachment = await createAttachmentAuth( { + iv: iv, filename: filename, mimetype: mimetype, size: size, diff --git a/src/controllers/file/helpers.ts b/src/controllers/file/helpers.ts index ba194e3..08a4bff 100644 --- a/src/controllers/file/helpers.ts +++ b/src/controllers/file/helpers.ts @@ -4,6 +4,7 @@ import type { IAttachment } from "./types.js"; const responseFullAttachment = (attachment: AttachmentWithChunks) => ({ id: attachment.id, + iv: attachment.iv, filename: attachment.filename, mimetype: attachment.mimetype, size: Number(attachment.size), diff --git a/src/controllers/file/types.ts b/src/controllers/file/types.ts index 40101b7..1335589 100644 --- a/src/controllers/file/types.ts +++ b/src/controllers/file/types.ts @@ -48,6 +48,7 @@ interface IPostUploadCommunityAvatarResponseSuccess { interface IAttachment { id: string; + iv: string; filename: string; mimetype: string; size: number; @@ -69,6 +70,7 @@ interface IGetAttachmentResponseError { interface IGetAttachmentResponseSuccess extends IAttachment {} interface IPostCreateAttachmentRequest { + iv: string; filename: string; mimetype: string; size: number; diff --git a/src/controllers/message/types.ts b/src/controllers/message/types.ts index 6afa5ce..4c05f6b 100644 --- a/src/controllers/message/types.ts +++ b/src/controllers/message/types.ts @@ -45,6 +45,7 @@ interface IPatchMessageParams { } interface IPatchMessageRequest { + iv: string; text: string; } diff --git a/src/controllers/role/helpers.ts b/src/controllers/role/helpers.ts index 81603a9..36fff58 100644 --- a/src/controllers/role/helpers.ts +++ b/src/controllers/role/helpers.ts @@ -6,6 +6,9 @@ const responseFullRole = (role: Role) => id: role.id, name: role.name, description: role.description, + color: role.color, + order: role.order, + showInMembers: role.showInMembers, communityId: role.communityId, permissions: role.permissions, creationDate: role.creationDate.getTime(), diff --git a/src/controllers/role/role.ts b/src/controllers/role/role.ts index f0de71d..8dbf456 100644 --- a/src/controllers/role/role.ts +++ b/src/controllers/role/role.ts @@ -21,6 +21,7 @@ import type { IPostUnassignRoleRequest, IPostUnassignRoleResponseError, IPostUnassignRoleResponseSuccess, + IGetPermissionsResponseSuccess, } from "./types.js"; import { assignRoleByIdAuth, @@ -32,6 +33,7 @@ import { } from "../../services/role/role.js"; import { API_ERROR } from "../errors.js"; import { responseFullRole } from "./helpers.js"; +import { PERMISSION } from "../../services/auth/permission.js"; const getRole = async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as IGetRoleParams; @@ -178,6 +180,15 @@ const postUnassignRole = async ( } as IPostUnassignRoleResponseSuccess; }; +const getPermissions = async ( + _request: FastifyRequest, + _reply: FastifyReply, +) => { + return { + permissions: Object.values(PERMISSION), + } as IGetPermissionsResponseSuccess; +}; + export { getRole, patchRole, @@ -185,4 +196,5 @@ export { postCreateRole, postAssignRole, postUnassignRole, + getPermissions, }; diff --git a/src/controllers/role/routes.ts b/src/controllers/role/routes.ts index 2c0cca2..4a09066 100644 --- a/src/controllers/role/routes.ts +++ b/src/controllers/role/routes.ts @@ -8,6 +8,7 @@ const roleRoutes = async (fastify: FastifyInstance) => { fastify.delete(`/:id`, controller.deleteRole); fastify.post(`/:id/assign`, controller.postAssignRole); fastify.post(`/:id/unassign`, controller.postUnassignRole); + fastify.get(`/permissions`, controller.getPermissions); }; export { roleRoutes }; diff --git a/src/controllers/role/types.ts b/src/controllers/role/types.ts index 2664d1a..082eccc 100644 --- a/src/controllers/role/types.ts +++ b/src/controllers/role/types.ts @@ -5,6 +5,9 @@ interface IRole { id: string; name: string; description: string; + color: string; + order: number; + showInMembers: boolean; communityId: string; permissions: string[]; creationDate: number; @@ -42,6 +45,9 @@ interface IPatchRoleParams { interface IPatchRoleRequest { name?: string; description?: string; + color?: string; + showInMembers?: boolean; + permissions?: PERMISSION[]; } interface IPatchRoleResponseError { @@ -105,6 +111,10 @@ interface IPostUnassignRoleResponseSuccess { userId: string; } +interface IGetPermissionsResponseSuccess { + permissions: PERMISSION[]; +} + export { type IRole, type IGetRoleParams, @@ -128,4 +138,5 @@ export { type IPostUnassignRoleRequest, type IPostUnassignRoleResponseError, type IPostUnassignRoleResponseSuccess, + type IGetPermissionsResponseSuccess, }; diff --git a/src/controllers/user/routes.ts b/src/controllers/user/routes.ts index f95b547..5c18c75 100644 --- a/src/controllers/user/routes.ts +++ b/src/controllers/user/routes.ts @@ -9,6 +9,10 @@ const userRoutes = async (fastify: FastifyInstance) => { fastify.delete(`/:id`, controller.deleteUser); fastify.get(`/:id/sessions`, controller.getSessions); fastify.get(`/:id/communities`, controller.getCommunities); + fastify.get( + `/:id/community/:communityId/roles`, + controller.getCommunityRoles, + ); }; export { userRoutes }; diff --git a/src/controllers/user/types.ts b/src/controllers/user/types.ts index 57cba21..cea8f7c 100644 --- a/src/controllers/user/types.ts +++ b/src/controllers/user/types.ts @@ -120,6 +120,28 @@ interface IGetCommunitiesResponseCommunity { avatar?: string; } +interface IGetCommunityRolesParams { + id: string; + communityId: string; +} + +interface IGetCommunityRolesResponseError { + id: string; + error: API_ERROR; +} + +interface IGetCommunityRolesResponseSuccess { + id: string; + communityId: string; + roles: IGetCommunityRolesResponseCommunity[]; +} + +interface IGetCommunityRolesResponseCommunity { + id: string; + name: string; + description: string; +} + export { type IUser, type IGetLoggedUserResponseError, @@ -145,4 +167,8 @@ export { type IGetCommunitiesResponseError, type IGetCommunitiesResponseSuccess, type IGetCommunitiesResponseCommunity, + type IGetCommunityRolesParams, + type IGetCommunityRolesResponseError, + type IGetCommunityRolesResponseSuccess, + type IGetCommunityRolesResponseCommunity, }; diff --git a/src/controllers/user/user.ts b/src/controllers/user/user.ts index 65e516c..89c90d5 100644 --- a/src/controllers/user/user.ts +++ b/src/controllers/user/user.ts @@ -21,6 +21,9 @@ import type { IGetCommunitiesParams, IGetCommunitiesResponseError, IGetCommunitiesResponseSuccess, + IGetCommunityRolesParams, + IGetCommunityRolesResponseError, + IGetCommunityRolesResponseSuccess, } from "./types.js"; import { getUserByIdAuth, @@ -30,6 +33,7 @@ import { createUserAuth, getUserCommunitiesByIdAuth, getLoggedUserAuth, + getUserCommunityRolesByIdAuth, } from "../../services/user/user.js"; import { API_ERROR } from "../errors.js"; import { responseFullUser } from "./helpers.js"; @@ -201,6 +205,44 @@ const getCommunities = async (request: FastifyRequest, reply: FastifyReply) => { } as IGetCommunitiesResponseSuccess; }; +const getCommunityRoles = async ( + request: FastifyRequest, + reply: FastifyReply, +) => { + const { id, communityId } = request.params as IGetCommunityRolesParams; + const authHeader = request.headers["authorization"]; + + const roles = await getUserCommunityRolesByIdAuth( + id, + communityId, + authHeader, + ); + if (!roles) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IGetCommunityRolesResponseError; + } + if (roles === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IGetCommunityRolesResponseError; + } + + return { + id: id, + communityId: communityId, + roles: roles.map((role) => ({ + id: role.id, + name: role.name, + description: role.description, + })), + } as IGetCommunityRolesResponseSuccess; +}; + export { getUserLogged, getUser, @@ -209,4 +251,5 @@ export { deleteUser, getSessions, getCommunities, + getCommunityRoles, }; diff --git a/src/index.ts b/src/index.ts index b8ac9c9..0747c9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { config } from "./config.js"; import { getCookieSecret } from "./services/auth/helpers.js"; import { testRoutes } from "./controllers/test/routes.js"; +import { announcementRoutes } from "./controllers/announcement/routes.js"; import { authRoutes } from "./controllers/auth/routes.js"; import { userRoutes } from "./controllers/user/routes.js"; import { sessionRoutes } from "./controllers/session/routes.js"; @@ -45,6 +46,7 @@ app.register(multipart, { }); app.register(testRoutes); +app.register(announcementRoutes, { prefix: "/api/v1/announcement" }); app.register(authRoutes, { prefix: "/api/v1/auth" }); app.register(userRoutes, { prefix: "/api/v1/user" }); app.register(sessionRoutes, { prefix: "/api/v1/session" }); diff --git a/src/services/announcement/announcement.ts b/src/services/announcement/announcement.ts new file mode 100644 index 0000000..d6abe14 --- /dev/null +++ b/src/services/announcement/announcement.ts @@ -0,0 +1,30 @@ +import { API_ERROR } from "../../controllers/errors.js"; +import { getUserFromAuth } from "../auth/helpers.js"; +import { SocketMessageTypes } from "../websocket/types.js"; +import { sendMessageToEveryone } from "../websocket/websocket.js"; +import type { ICreateAnnouncement } from "./types.js"; + +const createAnnouncement = async (create: ICreateAnnouncement) => { + sendMessageToEveryone({ + type: SocketMessageTypes.ANNOUNCEMENT, + payload: { + title: create.title, + text: create.text, + }, + }); +}; + +const createAnnouncementAuth = async ( + create: ICreateAnnouncement, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + + if (!authUser?.admin) { + //return API_ERROR.ACCESS_DENIED; + } + + return await createAnnouncement(create); +}; + +export { createAnnouncement, createAnnouncementAuth }; diff --git a/src/services/announcement/index.ts b/src/services/announcement/index.ts new file mode 100644 index 0000000..be74140 --- /dev/null +++ b/src/services/announcement/index.ts @@ -0,0 +1,2 @@ +export * from "./announcement.js"; +export * from "./types.js"; diff --git a/src/services/announcement/types.ts b/src/services/announcement/types.ts new file mode 100644 index 0000000..af99ef5 --- /dev/null +++ b/src/services/announcement/types.ts @@ -0,0 +1,6 @@ +interface ICreateAnnouncement { + title: string; + text: string; +} + +export { type ICreateAnnouncement }; diff --git a/src/services/auth/auth.ts b/src/services/auth/auth.ts index 47fa0c0..ae4215f 100644 --- a/src/services/auth/auth.ts +++ b/src/services/auth/auth.ts @@ -4,6 +4,7 @@ import { getDB } from "../../store/store.js"; import { createSessionCookie, createToken, + getRandomBytesBase64, getRandomBytesHex, hashPassword, verifyPassword, @@ -77,7 +78,7 @@ const loginUser = async (login: IUserLogin): Promise => { userId: user.id, name: sessionName, userAgent: login.userAgent, - storageSecret: `${getRandomBytesHex(32)};${getRandomBytesHex(12)}`, + storageSecret: `${getRandomBytesBase64(32)};${getRandomBytesBase64(12)}`, }, }); }; diff --git a/src/services/auth/helpers.ts b/src/services/auth/helpers.ts index fac2177..6bf4dc3 100644 --- a/src/services/auth/helpers.ts +++ b/src/services/auth/helpers.ts @@ -18,6 +18,10 @@ const getRandomBytesHex = (amount: number) => { return crypto.randomBytes(amount).toString("hex"); }; +const getRandomBytesBase64 = (amount: number) => { + return crypto.randomBytes(amount).toString("base64"); +}; + const createSessionCookie = () => { return getRandomBytesHex(32); }; @@ -243,6 +247,7 @@ export { getJwtSecret, getCookieSecret, getRandomBytesHex, + getRandomBytesBase64, createSessionCookie, createToken, verifyToken, diff --git a/src/services/channel/channel.ts b/src/services/channel/channel.ts index c5eb31b..a9abc95 100644 --- a/src/services/channel/channel.ts +++ b/src/services/channel/channel.ts @@ -177,6 +177,7 @@ const getChannelMessagesById = async ( id: string, ): Promise => { return await getDB().message.findMany({ + take: 50, include: { reactions: { select: { diff --git a/src/services/channel/types.ts b/src/services/channel/types.ts index 800adb6..7b400d6 100644 --- a/src/services/channel/types.ts +++ b/src/services/channel/types.ts @@ -5,6 +5,9 @@ interface ICreateChannel { interface IUpdateChannel { name?: string; + description?: string; + category?: string; + order?: number; } export { type ICreateChannel, type IUpdateChannel }; diff --git a/src/services/community/community.ts b/src/services/community/community.ts index 01ee850..5aec597 100644 --- a/src/services/community/community.ts +++ b/src/services/community/community.ts @@ -7,6 +7,8 @@ import { isUserOwnerOrAdmin, } from "../auth/helpers.js"; import { PERMISSION } from "../auth/permission.js"; +import { getChannelById } from "../channel/channel.js"; +import { getRoleById } from "../role/role.js"; import { getUserIdsInCommunity } from "../user/user.js"; import { SocketMessageTypes } from "../websocket/types.js"; import { sendMessageToUsersWS } from "../websocket/websocket.js"; @@ -16,7 +18,6 @@ import type { ICommunityChannel, ICommunityMember, ICommunityRole, - ICommunityInvite, ICreateInvite, } from "./types.js"; @@ -60,7 +61,7 @@ const updateCommunityById = async ( id: string, update: IUpdateCommunity, ): Promise => { - return await getDB().community.update({ + const updatedCommunity = await getDB().community.update({ where: { id: id, }, @@ -68,6 +69,17 @@ const updateCommunityById = async ( ...update, }, }); + + const userIds = await getUserIdsInCommunity(id); + + sendMessageToUsersWS(userIds, { + type: SocketMessageTypes.UPDATE_COMMUNITY, + payload: { + communityId: id, + }, + }); + + return updatedCommunity; }; const updateCommunityByIdAuth = async ( @@ -133,6 +145,7 @@ const getCommunityMembersById = async ( id: true, username: true, nickname: true, + avatar: true, }, }); }; @@ -170,6 +183,9 @@ const getCommunityChannelsById = async ( select: { id: true, name: true, + description: true, + category: true, + order: true, }, }); }; @@ -205,6 +221,9 @@ const getCommunityRolesById = async (id: string): Promise => { select: { id: true, name: true, + color: true, + order: true, + showInMembers: true, }, }); }; @@ -232,23 +251,18 @@ const getCommunityRolesByIdAuth = async ( return await getCommunityRolesById(id); }; -const getCommunityInvitesById = async ( - id: string, -): Promise => { +const getCommunityInvitesById = async (id: string): Promise => { return await getDB().invite.findMany({ where: { communityId: id, }, - select: { - id: true, - }, }); }; const getCommunityInvitesByIdAuth = async ( id: string, authHeader: string | undefined, -): Promise => { +): Promise => { const authUser = await getUserFromAuth(authHeader); const community = await getCommunityById(id); @@ -268,6 +282,118 @@ const getCommunityInvitesByIdAuth = async ( return await getCommunityInvitesById(id); }; +const updateCommunityChannelOrderById = async (id: string, order: string[]) => { + for (let i = 0; i < order.length; i++) { + const channelId = order[i]; + if (!channelId) { + continue; + } + + const channel = await getChannelById(channelId); + if (channel?.communityId !== id) { + continue; + } + + await getDB().channel.update({ + where: { + id: channelId, + }, + data: { + order: i, + }, + }); + } + + const userIds = await getUserIdsInCommunity(id); + + sendMessageToUsersWS(userIds, { + type: SocketMessageTypes.UPDATE_CHANNELS, + payload: { + communityId: id, + }, + }); +}; + +const updateCommunityChannelOrderByIdAuth = async ( + id: string, + order: string[], + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(id); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.CHANNELS_MANAGE], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + await updateCommunityChannelOrderById(id, order); +}; + +const updateCommunityRoleOrderById = async (id: string, order: string[]) => { + for (let i = 0; i < order.length; i++) { + const roleId = order[i]; + if (!roleId) { + continue; + } + + const role = await getRoleById(roleId); + if (role?.communityId !== id) { + continue; + } + + await getDB().role.update({ + where: { + id: roleId, + }, + data: { + order: i, + }, + }); + } + + const userIds = await getUserIdsInCommunity(id); + + sendMessageToUsersWS(userIds, { + type: SocketMessageTypes.UPDATE_ROLES, + payload: { + communityId: id, + }, + }); +}; + +const updateCommunityRoleOrderByIdAuth = async ( + id: string, + order: string[], + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(id); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.ROLES_MANAGE], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + await updateCommunityRoleOrderById(id, order); +}; + const createInvite = async ( id: string, creatorId: string, @@ -377,6 +503,10 @@ export { getCommunityRolesByIdAuth, getCommunityInvitesById, getCommunityInvitesByIdAuth, + updateCommunityChannelOrderById, + updateCommunityChannelOrderByIdAuth, + updateCommunityRoleOrderById, + updateCommunityRoleOrderByIdAuth, createInvite, createInviteAuth, deleteMemberById, diff --git a/src/services/community/types.ts b/src/services/community/types.ts index 2f4f030..a507562 100644 --- a/src/services/community/types.ts +++ b/src/services/community/types.ts @@ -12,21 +12,23 @@ interface ICommunityMember { id: string; username: string; nickname?: string | null; - avatar?: string; + avatar?: string | null; } interface ICommunityChannel { id: string; name: string; + description: string | null; + category: string | null; + order: number | null; } interface ICommunityRole { id: string; name: string; -} - -interface ICommunityInvite { - id: string; + color: string | null; + order: number | null; + showInMembers?: boolean | null; } interface ICreateInvite { @@ -41,6 +43,5 @@ export { type ICommunityMember, type ICommunityChannel, type ICommunityRole, - type ICommunityInvite, type ICreateInvite, }; diff --git a/src/services/file/types.ts b/src/services/file/types.ts index 1063d5c..f10629b 100644 --- a/src/services/file/types.ts +++ b/src/services/file/types.ts @@ -7,6 +7,7 @@ interface AttachmentWithChunks extends Attachment { } interface ICreateAttachment { + iv: string; filename: string; mimetype: string; size: number; diff --git a/src/services/invite/invite.ts b/src/services/invite/invite.ts index d51dc0c..98c6dde 100644 --- a/src/services/invite/invite.ts +++ b/src/services/invite/invite.ts @@ -102,6 +102,10 @@ const acceptInviteByIdAuth = async ( return API_ERROR.ACCESS_DENIED; } + if (!isInviteValid(invite)) { + return API_ERROR.ACCESS_DENIED; + } + if (await isUserInCommunity(authUser, community)) { return API_ERROR.ACCESS_DENIED; } diff --git a/src/services/message/message.ts b/src/services/message/message.ts index 5186901..615cb06 100644 --- a/src/services/message/message.ts +++ b/src/services/message/message.ts @@ -250,14 +250,6 @@ const deleteMessageById = async ( }, }); - for (const attachmentId of deletedMessage.attachments) { - await getDB().attachment.delete({ - where: { - id: attachmentId.id, - }, - }); - } - const userIds = await getUserIdsInCommunityReadMessagesPermission(communityId); diff --git a/src/services/message/types.ts b/src/services/message/types.ts index 69b1825..5a1abbf 100644 --- a/src/services/message/types.ts +++ b/src/services/message/types.ts @@ -18,6 +18,7 @@ interface ICreateMessage { } interface IUpdateMessage { + iv: string; text: string; } diff --git a/src/services/role/types.ts b/src/services/role/types.ts index 147a989..d0d6aeb 100644 --- a/src/services/role/types.ts +++ b/src/services/role/types.ts @@ -8,6 +8,10 @@ interface ICreateRole { interface IUpdateRole { name?: string; + description?: string; + color?: string; + order?: number; + showInMembers?: boolean; permissions?: PERMISSION[]; } diff --git a/src/services/user/user.ts b/src/services/user/user.ts index b1e21df..5ec6db0 100644 --- a/src/services/user/user.ts +++ b/src/services/user/user.ts @@ -2,16 +2,19 @@ import type { User, Session, Community, + Role, } from "../../generated/prisma/client.js"; import { getUserFromAuth, getUserPermissions, + isUserAllowed, isUserOwnerOrAdmin, } from "../auth/helpers.js"; import { getDB } from "../../store/store.js"; import { API_ERROR } from "../../controllers/errors.js"; import type { ICreateUser, IUpdateUser } from "./types.js"; import { PERMISSION } from "../auth/permission.js"; +import { getCommunityById } from "../community/community.js"; const communitiesWithReadableUsersCache = new Map>(); @@ -105,11 +108,7 @@ const getUserByIdAuth = async ( const authUser = await getUserFromAuth(authHeader); const user = await getUserById(id); - if ( - !(await isUserOwnerOrAdmin(authUser, { - user: user, - })) - ) { + if (!authUser) { return API_ERROR.ACCESS_DENIED; } @@ -254,6 +253,46 @@ const getUserCommunitiesByIdAuth = async ( return communities; }; +const getUserCommunityRolesById = async ( + id: string, + communityId: string, +): Promise => { + return await getDB().role.findMany({ + where: { + communityId: communityId, + users: { + some: { + id: id, + }, + }, + }, + }); +}; + +const getUserCommunityRolesByIdAuth = async ( + id: string, + communityId: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(communityId); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.MEMBERS_READ, PERMISSION.ROLES_READ], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await getUserCommunityRolesById(id, communityId); +}; + const getUserIdsInCommunity = async ( communityId: string, ): Promise => { @@ -318,6 +357,8 @@ export { getUserSessionsByIdAuth, getUserCommunitiesById, getUserCommunitiesByIdAuth, + getUserCommunityRolesById, + getUserCommunityRolesByIdAuth, getUserIdsInCommunity, getUserIdsInCommunityReadMessagesPermission, getUserIdsWithRole, diff --git a/src/services/websocket/types.ts b/src/services/websocket/types.ts index a35d7f3..cbea236 100644 --- a/src/services/websocket/types.ts +++ b/src/services/websocket/types.ts @@ -14,6 +14,7 @@ enum SocketMessageTypes { ANNOUNCEMENT = "ANNOUNCEMENT", SET_MESSAGE = "SET_MESSAGE", DELETE_MESSAGE = "DELETE_MESSAGE", + UPDATE_COMMUNITY = "UPDATE_COMMUNITY", UPDATE_CHANNELS = "UPDATE_CHANNELS", UPDATE_ROLES = "UPDATE_ROLES", UPDATE_MEMBERS = "UPDATE_MEMBERS", @@ -28,7 +29,7 @@ type SocketMessage = type: SocketMessageTypes.ANNOUNCEMENT; payload: { title: string; - description: string; + text: string; }; } | { @@ -51,6 +52,12 @@ type SocketMessage = communityId: string; }; } + | { + type: SocketMessageTypes.UPDATE_COMMUNITY; + payload: { + communityId: string; + }; + } | { type: SocketMessageTypes.UPDATE_ROLES; payload: { diff --git a/src/services/websocket/websocket.ts b/src/services/websocket/websocket.ts index 5d65339..137f32d 100644 --- a/src/services/websocket/websocket.ts +++ b/src/services/websocket/websocket.ts @@ -53,6 +53,14 @@ const onMessageWsHandler = (connection: ISocketConnection) => { }; }; +const sendMessageToEveryone = (message: SocketMessage) => { + userConnections?.forEach((connections) => { + connections?.forEach((connection) => { + connection.socket.send(JSON.stringify(message)); + }); + }); +}; + const sendMessageToUserWS = (userId: string, message: SocketMessage) => { const connections = userConnections.get(userId); @@ -70,6 +78,7 @@ const sendMessageToUsersWS = (userIds: string[], message: SocketMessage) => { export { userConnections, handleNewWebSocket, + sendMessageToEveryone, sendMessageToUserWS, sendMessageToUsersWS, };