From ddcc591d1289615a097bf7417c0b6b156040171f96f123ad9972feab112c0ff7 Mon Sep 17 00:00:00 2001 From: aslan Date: Mon, 29 Dec 2025 02:02:56 +0100 Subject: [PATCH] Add more services; Version 0.3.3 --- package.json | 2 +- .../migration.sql | 11 ++ .../migration.sql | 17 +++ prisma/schema.prisma | 10 +- src/controllers/channel/channel.ts | 69 +++++++++- src/controllers/channel/routes.ts | 2 + src/controllers/channel/types.ts | 40 ++++++ src/controllers/community/community.ts | 33 +++++ src/controllers/community/routes.ts | 1 + src/controllers/community/types.ts | 16 +++ src/controllers/role/role.ts | 121 +++++++++++++++++- src/controllers/role/routes.ts | 3 + src/controllers/role/types.ts | 61 +++++++++ src/controllers/user/routes.ts | 2 + src/controllers/user/types.ts | 40 ++++++ src/controllers/user/user.ts | 56 +++++++- src/services/channel/channel.ts | 84 +++++++++++- src/services/channel/types.ts | 6 +- src/services/community/community.ts | 32 ++++- src/services/invite/invite.ts | 4 +- src/services/role/role.ts | 91 ++++++++++++- src/services/role/types.ts | 7 +- src/services/user/types.ts | 10 +- src/services/user/user.ts | 51 +++++++- tests/1.user.test.js | 40 ++++++ tests/2.community.test.js | 116 +++++++++++++++-- 26 files changed, 887 insertions(+), 38 deletions(-) create mode 100644 prisma/migrations/20251228175359_on_delete_roles_channels/migration.sql create mode 100644 prisma/migrations/20251228175529_on_delete_invite/migration.sql diff --git a/package.json b/package.json index 3946f53..499416f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tether", - "version": "0.3.1", + "version": "0.3.3", "description": "Communication server using the Nexlink protocol", "repository": { "type": "git", diff --git a/prisma/migrations/20251228175359_on_delete_roles_channels/migration.sql b/prisma/migrations/20251228175359_on_delete_roles_channels/migration.sql new file mode 100644 index 0000000..e179d11 --- /dev/null +++ b/prisma/migrations/20251228175359_on_delete_roles_channels/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "Channel" DROP CONSTRAINT "Channel_communityId_fkey"; + +-- DropForeignKey +ALTER TABLE "Role" DROP CONSTRAINT "Role_communityId_fkey"; + +-- AddForeignKey +ALTER TABLE "Channel" ADD CONSTRAINT "Channel_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Role" ADD CONSTRAINT "Role_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20251228175529_on_delete_invite/migration.sql b/prisma/migrations/20251228175529_on_delete_invite/migration.sql new file mode 100644 index 0000000..62a8394 --- /dev/null +++ b/prisma/migrations/20251228175529_on_delete_invite/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE "Invite" DROP CONSTRAINT "Invite_communityId_fkey"; + +-- DropForeignKey +ALTER TABLE "Invite" DROP CONSTRAINT "Invite_creatorId_fkey"; + +-- DropForeignKey +ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invite" ADD CONSTRAINT "Invite_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invite" ADD CONSTRAINT "Invite_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 401d22e..8f3c152 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,7 +24,7 @@ model Community { model Channel { id String @id @unique @default(uuid()) name String - community Community @relation(fields: [communityId], references: [id]) + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) communityId String creationDate DateTime @default(now()) } @@ -32,7 +32,7 @@ model Channel { model Role { id String @id @unique @default(uuid()) name String - community Community @relation(fields: [communityId], references: [id]) + community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) communityId String users User[] @relation(name: "UsersRolesToUsers") permissions String[] @@ -57,7 +57,7 @@ model User { model Session { id String @id @unique @default(uuid()) - owner User @relation(fields: [userId], references: [id]) + owner User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String token String creationDate DateTime @default(now()) @@ -65,9 +65,9 @@ model Session { model Invite { id String @id @unique @default(uuid()) - Community Community @relation(fields: [communityId], references: [id]) + Community Community @relation(fields: [communityId], references: [id], onDelete: Cascade) communityId String - User User @relation(fields: [creatorId], references: [id]) + User User @relation(fields: [creatorId], references: [id], onDelete: Cascade) creatorId String totalInvites Int @default(0) remainingInvites Int @default(0) diff --git a/src/controllers/channel/channel.ts b/src/controllers/channel/channel.ts index b4a1acc..fc1e6c5 100644 --- a/src/controllers/channel/channel.ts +++ b/src/controllers/channel/channel.ts @@ -6,10 +6,19 @@ import type { IPostCreateChannelRequest, IPostCreateChannelResponseError, IPostCreateChannelResponseSuccess, + IPatchChannelParams, + IPatchChannelRequest, + IPatchChannelResponseError, + IPatchChannelResponseSuccess, + IDeleteChannelParams, + IDeleteChannelResponseError, + IDeleteChannelResponseSuccess, } from "./types.js"; import { createChannelAuth, + deleteChannelByIdAuth, getChannelByIdAuth, + updateChannelByIdAuth, } from "../../services/channel/channel.js"; import { API_ERROR } from "../errors.js"; @@ -63,4 +72,62 @@ const postCreateChannel = async ( } as IPostCreateChannelResponseSuccess; }; -export { getChannel, postCreateChannel }; +const patchChannel = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IPatchChannelParams; + const patchChannelRequest = request.body as IPatchChannelRequest; + const authHeader = request.headers["authorization"]; + + const channel = await updateChannelByIdAuth( + id, + patchChannelRequest, + authHeader, + ); + if (!channel) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IPatchChannelResponseError; + } + if (channel === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IPatchChannelResponseError; + } + + return { + id: channel.id, + name: channel.name, + communityId: channel.communityId, + } as IPatchChannelResponseSuccess; +}; + +const deleteChannel = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IDeleteChannelParams; + const authHeader = request.headers["authorization"]; + + const channel = await deleteChannelByIdAuth(id, authHeader); + if (!channel) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IDeleteChannelResponseError; + } + if (channel === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IDeleteChannelResponseError; + } + + return { + id: channel.id, + communityId: channel.communityId, + } as IDeleteChannelResponseSuccess; +}; + +export { getChannel, postCreateChannel, patchChannel, deleteChannel }; diff --git a/src/controllers/channel/routes.ts b/src/controllers/channel/routes.ts index d643289..9646137 100644 --- a/src/controllers/channel/routes.ts +++ b/src/controllers/channel/routes.ts @@ -4,6 +4,8 @@ import * as controller from "./channel.js"; const channelRoutes = async (fastify: FastifyInstance) => { fastify.get(`/:id`, controller.getChannel); fastify.post(`/`, controller.postCreateChannel); + fastify.patch(`/:id`, controller.patchChannel); + fastify.delete(`/:id`, controller.deleteChannel); }; export { channelRoutes }; diff --git a/src/controllers/channel/types.ts b/src/controllers/channel/types.ts index 8f38c49..05b488e 100644 --- a/src/controllers/channel/types.ts +++ b/src/controllers/channel/types.ts @@ -32,6 +32,39 @@ interface IPostCreateChannelResponseSuccess { communityId: string; } +interface IPatchChannelParams { + id: string; +} + +interface IPatchChannelRequest { + name?: string; +} + +interface IPatchChannelResponseError { + id: string; + error: API_ERROR; +} + +interface IPatchChannelResponseSuccess { + id: string; + name: string; + communityId: string; +} + +interface IDeleteChannelParams { + id: string; +} + +interface IDeleteChannelResponseError { + id: string; + error: API_ERROR; +} + +interface IDeleteChannelResponseSuccess { + id: string; + communityId: string; +} + export { type IGetChannelParams, type IGetChannelResponseError, @@ -39,4 +72,11 @@ export { type IPostCreateChannelRequest, type IPostCreateChannelResponseError, type IPostCreateChannelResponseSuccess, + type IPatchChannelParams, + type IPatchChannelRequest, + type IPatchChannelResponseError, + type IPatchChannelResponseSuccess, + type IDeleteChannelParams, + type IDeleteChannelResponseError, + type IDeleteChannelResponseSuccess, }; diff --git a/src/controllers/community/community.ts b/src/controllers/community/community.ts index 21cc5f8..b231810 100644 --- a/src/controllers/community/community.ts +++ b/src/controllers/community/community.ts @@ -10,6 +10,9 @@ import type { IPatchCommunityRequest, IPatchCommunityResponseError, IPatchCommunityResponseSuccess, + IDeleteCommunityParams, + IDeleteCommunityResponseError, + IDeleteCommunityResponseSuccess, IGetMembersParams, IGetMembersResponseError, IGetMembersResponseSuccess, @@ -32,6 +35,7 @@ import { getCommunityMembersByIdAuth, getCommunityRolesByIdAuth, createInviteAuth, + deleteCommunityByIdAuth, } from "../../services/community/community.js"; import { API_ERROR } from "../errors.js"; import type { ICreateInvite } from "../../services/community/types.js"; @@ -115,6 +119,34 @@ const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => { } as IPatchCommunityResponseSuccess; }; +const deleteCommunity = async ( + request: FastifyRequest, + reply: FastifyReply, +) => { + const { id } = request.params as IDeleteCommunityParams; + const authHeader = request.headers["authorization"]; + + const community = await deleteCommunityByIdAuth(id, authHeader); + if (!community) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IDeleteCommunityResponseError; + } + if (community === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IDeleteCommunityResponseError; + } + + return { + id: community.id, + } as IDeleteCommunityResponseSuccess; +}; + const getMembers = async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as IGetMembersParams; const authHeader = request.headers["authorization"]; @@ -253,6 +285,7 @@ export { getCommunity, postCreateCommunity, patchCommunity, + deleteCommunity, getMembers, getChannels, getRoles, diff --git a/src/controllers/community/routes.ts b/src/controllers/community/routes.ts index e95a4d2..5a18b99 100644 --- a/src/controllers/community/routes.ts +++ b/src/controllers/community/routes.ts @@ -5,6 +5,7 @@ const communityRoutes = async (fastify: FastifyInstance) => { fastify.get(`/:id`, controller.getCommunity); fastify.post(`/`, controller.postCreateCommunity); fastify.patch(`/:id`, controller.patchCommunity); + fastify.delete(`/:id`, controller.deleteCommunity); fastify.get(`/:id/members`, controller.getMembers); fastify.get(`/:id/channels`, controller.getChannels); fastify.get(`/:id/roles`, controller.getRoles); diff --git a/src/controllers/community/types.ts b/src/controllers/community/types.ts index 9243ce6..4dcc910 100644 --- a/src/controllers/community/types.ts +++ b/src/controllers/community/types.ts @@ -54,6 +54,19 @@ interface IPatchCommunityResponseSuccess { description: string; } +interface IDeleteCommunityParams { + id: string; +} + +interface IDeleteCommunityResponseError { + id: string; + error: API_ERROR; +} + +interface IDeleteCommunityResponseSuccess { + id: string; +} + interface IGetMembersParams { id: string; } @@ -144,6 +157,9 @@ export { type IPatchCommunityRequest, type IPatchCommunityResponseError, type IPatchCommunityResponseSuccess, + type IDeleteCommunityParams, + type IDeleteCommunityResponseError, + type IDeleteCommunityResponseSuccess, type IGetMembersParams, type IGetMembersResponseError, type IGetMembersResponseSuccess, diff --git a/src/controllers/role/role.ts b/src/controllers/role/role.ts index 2281cce..0a26d2e 100644 --- a/src/controllers/role/role.ts +++ b/src/controllers/role/role.ts @@ -3,9 +3,19 @@ import type { IGetRoleParams, IGetRoleResponseError, IGetRoleResponseSuccess, + IGetRolePermissionsParams, + IGetRolePermissionsResponseError, + IGetRolePermissionsResponseSuccess, IPostCreateRoleRequest, IPostCreateRoleResponseError, IPostCreateRoleResponseSuccess, + IPatchRoleParams, + IPatchRoleRequest, + IPatchRoleResponseError, + IPatchRoleResponseSuccess, + IDeleteRoleParams, + IDeleteRoleResponseError, + IDeleteRoleResponseSuccess, IPostAssignRoleParams, IPostAssignRoleRequest, IPostAssignRoleResponseError, @@ -18,8 +28,10 @@ import type { import { assignRoleByIdAuth, createRoleAuth, + deleteRoleByIdAuth, getRoleByIdAuth, unassignRoleByIdAuth, + updateRoleByIdAuth, } from "../../services/role/role.js"; import { API_ERROR } from "../errors.js"; @@ -51,6 +63,38 @@ const getRole = async (request: FastifyRequest, reply: FastifyReply) => { } as IGetRoleResponseSuccess; }; +const getRolePermissions = async ( + request: FastifyRequest, + reply: FastifyReply, +) => { + const { id } = request.params as IGetRolePermissionsParams; + const authHeader = request.headers["authorization"]; + + const role = await getRoleByIdAuth(id, authHeader); + if (!role) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IGetRolePermissionsResponseError; + } + if (role === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IGetRolePermissionsResponseError; + } + + return { + id: role.id, + name: role.name, + communityId: role.communityId, + permissions: role.permissions, + creationDate: role.creationDate.getTime(), + } as IGetRolePermissionsResponseSuccess; +}; + const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => { const createRoleRequest = request.body as IPostCreateRoleRequest; const authHeader = request.headers["authorization"]; @@ -70,12 +114,73 @@ const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => { } as IPostCreateRoleResponseSuccess; }; +const patchRole = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IPatchRoleParams; + const patchRoleRequest = request.body as IPatchRoleRequest; + const authHeader = request.headers["authorization"]; + + const role = await updateRoleByIdAuth(id, patchRoleRequest, authHeader); + if (!role) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IPatchRoleResponseError; + } + if (role === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IPatchRoleResponseError; + } + + return { + id: role.id, + name: role.name, + communityId: role.communityId, + permissions: role.permissions, + } as IPatchRoleResponseSuccess; +}; + +const deleteRole = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IDeleteRoleParams; + const authHeader = request.headers["authorization"]; + + const role = await deleteRoleByIdAuth(id, authHeader); + if (!role) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IDeleteRoleResponseError; + } + if (role === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IDeleteRoleResponseError; + } + + return { + id: role.id, + communityId: role.communityId, + } as IDeleteRoleResponseSuccess; +}; + const postAssignRole = async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as IPostAssignRoleParams; const { userId } = request.body as IPostAssignRoleRequest; const authHeader = request.headers["authorization"]; const role = await assignRoleByIdAuth(id, userId, authHeader); + if (!role) { + reply.status(404); + return { + error: API_ERROR.NOT_FOUND, + } as IPostAssignRoleResponseError; + } if (role === API_ERROR.ACCESS_DENIED) { reply.status(403); return { @@ -106,6 +211,12 @@ const postUnassignRole = async ( error: API_ERROR.ACCESS_DENIED, } as IPostUnassignRoleResponseError; } + if (!role) { + reply.status(404); + return { + error: API_ERROR.NOT_FOUND, + } as IPostUnassignRoleResponseError; + } return { id: role.id, @@ -115,4 +226,12 @@ const postUnassignRole = async ( } as IPostUnassignRoleResponseSuccess; }; -export { getRole, postCreateRole, postAssignRole, postUnassignRole }; +export { + getRole, + getRolePermissions, + patchRole, + deleteRole, + postCreateRole, + postAssignRole, + postUnassignRole, +}; diff --git a/src/controllers/role/routes.ts b/src/controllers/role/routes.ts index 3a57146..fa72d9e 100644 --- a/src/controllers/role/routes.ts +++ b/src/controllers/role/routes.ts @@ -3,7 +3,10 @@ import * as controller from "./role.js"; const roleRoutes = async (fastify: FastifyInstance) => { fastify.get(`/:id`, controller.getRole); + fastify.get(`/:id/permissions`, controller.getRolePermissions); fastify.post(`/`, controller.postCreateRole); + fastify.patch(`/:id`, controller.patchRole); + fastify.delete(`/:id`, controller.deleteRole); fastify.post(`/:id/assign`, controller.postAssignRole); fastify.post(`/:id/unassign`, controller.postUnassignRole); }; diff --git a/src/controllers/role/types.ts b/src/controllers/role/types.ts index a9a0e2e..3910958 100644 --- a/src/controllers/role/types.ts +++ b/src/controllers/role/types.ts @@ -17,6 +17,23 @@ interface IGetRoleResponseSuccess { creationDate: number; } +interface IGetRolePermissionsParams { + id: string; +} + +interface IGetRolePermissionsResponseError { + id: string; + error: API_ERROR; +} + +interface IGetRolePermissionsResponseSuccess { + id: string; + name: string; + communityId: string; + permissions: PERMISSION[]; + creationDate: number; +} + interface IPostCreateRoleRequest { name: string; communityId: string; @@ -34,6 +51,40 @@ interface IPostCreateRoleResponseSuccess { communityId: string; } +interface IPatchRoleParams { + id: string; +} + +interface IPatchRoleRequest { + name?: string; +} + +interface IPatchRoleResponseError { + id: string; + error: API_ERROR; +} + +interface IPatchRoleResponseSuccess { + id: string; + name: string; + communityId: string; + permissions: PERMISSION[]; +} + +interface IDeleteRoleParams { + id: string; +} + +interface IDeleteRoleResponseError { + id: string; + error: API_ERROR; +} + +interface IDeleteRoleResponseSuccess { + id: string; + communityId: string; +} + interface IPostAssignRoleParams { id: string; } @@ -78,9 +129,19 @@ export { type IGetRoleParams, type IGetRoleResponseError, type IGetRoleResponseSuccess, + type IGetRolePermissionsParams, + type IGetRolePermissionsResponseError, + type IGetRolePermissionsResponseSuccess, type IPostCreateRoleRequest, type IPostCreateRoleResponseError, type IPostCreateRoleResponseSuccess, + type IPatchRoleParams, + type IPatchRoleRequest, + type IPatchRoleResponseError, + type IPatchRoleResponseSuccess, + type IDeleteRoleParams, + type IDeleteRoleResponseError, + type IDeleteRoleResponseSuccess, type IPostAssignRoleParams, type IPostAssignRoleRequest, type IPostAssignRoleResponseError, diff --git a/src/controllers/user/routes.ts b/src/controllers/user/routes.ts index 79a1750..23981fe 100644 --- a/src/controllers/user/routes.ts +++ b/src/controllers/user/routes.ts @@ -3,7 +3,9 @@ import * as controller from "./user.js"; const userRoutes = async (fastify: FastifyInstance) => { fastify.get(`/:id`, controller.getUser); + fastify.post(`/`, controller.postCreateUser); fastify.patch(`/:id`, controller.patchUser); + fastify.delete(`/:id`, controller.deleteUser); fastify.get(`/:id/sessions`, controller.getSessions); }; diff --git a/src/controllers/user/types.ts b/src/controllers/user/types.ts index dca63f0..4e2ccbe 100644 --- a/src/controllers/user/types.ts +++ b/src/controllers/user/types.ts @@ -19,6 +19,27 @@ interface IGetUserResponseSuccess { lastLogin: number; } +interface IPostCreateUserRequest { + username: string; + password: string; + email?: string; + description?: string; + admin?: boolean; +} + +interface IPostCreateUserResponseError { + id: string; + error: API_ERROR; +} + +interface IPostCreateUserResponseSuccess { + id: string; + username: string; + email: string; + description: string; + admin: boolean; +} + interface IPatchUserParams { id: string; } @@ -39,6 +60,19 @@ interface IPatchUserResponseSuccess { description: string; } +interface IDeleteUserParams { + id: string; +} + +interface IDeleteUserResponseError { + id: string; + error: API_ERROR; +} + +interface IDeleteUserResponseSuccess { + id: string; +} + interface IGetSessionsParams { id: string; } @@ -62,10 +96,16 @@ export { type IGetUserParams, type IGetUserResponseError, type IGetUserResponseSuccess, + type IPostCreateUserRequest, + type IPostCreateUserResponseError, + type IPostCreateUserResponseSuccess, type IPatchUserParams, type IPatchUserRequest, type IPatchUserResponseError, type IPatchUserResponseSuccess, + type IDeleteUserParams, + type IDeleteUserResponseError, + type IDeleteUserResponseSuccess, type IGetSessionsParams, type IGetSessionsResponseError, type IGetSessionsResponseSuccess, diff --git a/src/controllers/user/user.ts b/src/controllers/user/user.ts index ea4bbfc..5f01936 100644 --- a/src/controllers/user/user.ts +++ b/src/controllers/user/user.ts @@ -3,10 +3,16 @@ import type { IGetUserParams, IGetUserResponseError, IGetUserResponseSuccess, + IPostCreateUserRequest, + IPostCreateUserResponseError, + IPostCreateUserResponseSuccess, IPatchUserParams, IPatchUserRequest, IPatchUserResponseError, IPatchUserResponseSuccess, + IDeleteUserParams, + IDeleteUserResponseError, + IDeleteUserResponseSuccess, IGetSessionsParams, IGetSessionsResponseError, IGetSessionsResponseSuccess, @@ -15,6 +21,8 @@ import { getUserByIdAuth, updateUserByIdAuth, getUserSessionsByIdAuth, + deleteUserByIdAuth, + createUserAuth, } from "../../services/user/user.js"; import { API_ERROR } from "../errors.js"; @@ -49,6 +57,27 @@ const getUser = async (request: FastifyRequest, reply: FastifyReply) => { } as IGetUserResponseSuccess; }; +const postCreateUser = async (request: FastifyRequest, reply: FastifyReply) => { + const createUserRequest = request.body as IPostCreateUserRequest; + const authHeader = request.headers["authorization"]; + + const user = await createUserAuth(createUserRequest, authHeader); + if (user === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + error: API_ERROR.ACCESS_DENIED, + } as IPostCreateUserResponseError; + } + + return { + id: user.id, + username: user.username, + email: user.email, + description: user.description, + admin: user.admin, + } as IPostCreateUserResponseSuccess; +}; + const patchUser = async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as IPatchUserParams; const patchUserRequest = request.body as IPatchUserRequest; @@ -77,6 +106,31 @@ const patchUser = async (request: FastifyRequest, reply: FastifyReply) => { } as IPatchUserResponseSuccess; }; +const deleteUser = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IDeleteUserParams; + const authHeader = request.headers["authorization"]; + + const user = await deleteUserByIdAuth(id, authHeader); + if (!user) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IDeleteUserResponseError; + } + if (user === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IDeleteUserResponseError; + } + + return { + id: user.id, + } as IDeleteUserResponseSuccess; +}; + const getSessions = async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as IGetSessionsParams; const authHeader = request.headers["authorization"]; @@ -106,4 +160,4 @@ const getSessions = async (request: FastifyRequest, reply: FastifyReply) => { } as IGetSessionsResponseSuccess; }; -export { getUser, patchUser, getSessions }; +export { getUser, postCreateUser, patchUser, deleteUser, getSessions }; diff --git a/src/services/channel/channel.ts b/src/services/channel/channel.ts index f270e28..9505beb 100644 --- a/src/services/channel/channel.ts +++ b/src/services/channel/channel.ts @@ -4,7 +4,7 @@ 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 { ICreateChannel } from "./types.js"; +import type { ICreateChannel, IUpdateChannel } from "./types.js"; const getChannelById = async (id: string): Promise => { return await getDB().channel.findUnique({ @@ -24,7 +24,7 @@ const getChannelByIdAuth = async ( !(await isUserAllowed( authUser, { - channel: channel, + community: community, }, community, [PERMISSION.CHANNELS_READ], @@ -67,4 +67,82 @@ const createChannelAuth = async ( return await createChannel(create); }; -export { getChannelById, getChannelByIdAuth, createChannel, createChannelAuth }; +const updateChannelById = async ( + id: string, + update: IUpdateChannel, +): Promise => { + return await getDB().channel.update({ + where: { + id: id, + }, + data: { + ...update, + }, + }); +}; + +const updateChannelByIdAuth = async ( + id: string, + update: IUpdateChannel, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const channel = await getChannelById(id); + const community = await getCommunityById(channel?.communityId ?? ""); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.CHANNELS_MANAGE], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await updateChannelById(id, update); +}; + +const deleteChannelById = async (id: string): Promise => { + return await getDB().channel.delete({ + where: { id: id }, + }); +}; + +const deleteChannelByIdAuth = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const channel = await getChannelById(id); + const community = await getCommunityById(channel?.communityId ?? ""); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.CHANNELS_MANAGE], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await deleteChannelById(id); +}; + +export { + getChannelById, + getChannelByIdAuth, + createChannel, + createChannelAuth, + updateChannelById, + updateChannelByIdAuth, + deleteChannelById, + deleteChannelByIdAuth, +}; diff --git a/src/services/channel/types.ts b/src/services/channel/types.ts index 97fa273..800adb6 100644 --- a/src/services/channel/types.ts +++ b/src/services/channel/types.ts @@ -3,4 +3,8 @@ interface ICreateChannel { communityId: string; } -export { type ICreateChannel }; +interface IUpdateChannel { + name?: string; +} + +export { type ICreateChannel, type IUpdateChannel }; diff --git a/src/services/community/community.ts b/src/services/community/community.ts index 642561d..ebc4f97 100644 --- a/src/services/community/community.ts +++ b/src/services/community/community.ts @@ -1,7 +1,11 @@ import { API_ERROR } from "../../controllers/errors.js"; import type { Community, Invite, User } from "../../generated/prisma/client.js"; import { getDB } from "../../store/store.js"; -import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js"; +import { + getUserFromAuth, + isUserAllowed, + isUserOwnerOrAdmin, +} from "../auth/helpers.js"; import { PERMISSION } from "../auth/permission.js"; import type { ICreateCommunity, @@ -86,6 +90,30 @@ const updateCommunityByIdAuth = async ( return await updateCommunityById(id, update); }; +const deleteCommunityById = async (id: string): Promise => { + return await getDB().community.delete({ + where: { id: id }, + }); +}; + +const deleteCommunityByIdAuth = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(id); + + if ( + !(await isUserOwnerOrAdmin(authUser, { + community: community, + })) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await deleteCommunityById(id); +}; + const getCommunityMembersById = async ( id: string, ): Promise => { @@ -244,6 +272,8 @@ export { createCommunityAuth, updateCommunityById, updateCommunityByIdAuth, + deleteCommunityById, + deleteCommunityByIdAuth, getCommunityMembersById, getCommunityMembersByIdAuth, getCommunityChannelsById, diff --git a/src/services/invite/invite.ts b/src/services/invite/invite.ts index 23dd2f9..e5597a4 100644 --- a/src/services/invite/invite.ts +++ b/src/services/invite/invite.ts @@ -26,7 +26,7 @@ const deleteInviteByIdAuth = async ( authHeader: string | undefined, ): Promise => { const authUser = await getUserFromAuth(authHeader); - const invite = await deleteInviteById(id); + const invite = await getInviteById(id); const community = await getCommunityById(invite?.communityId ?? ""); if ( @@ -42,7 +42,7 @@ const deleteInviteByIdAuth = async ( return API_ERROR.ACCESS_DENIED; } - return invite; + return await deleteInviteById(id); }; const acceptInviteById = async ( diff --git a/src/services/role/role.ts b/src/services/role/role.ts index ec1267a..831b955 100644 --- a/src/services/role/role.ts +++ b/src/services/role/role.ts @@ -4,7 +4,7 @@ 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 { ICreateRole } from "./types.js"; +import type { ICreateRole, IUpdateRole } from "./types.js"; const getRoleById = async (id: string): Promise => { return await getDB().role.findUnique({ @@ -24,7 +24,7 @@ const getRoleByIdAuth = async ( !(await isUserAllowed( authUser, { - role: role, + community: community, }, community, [PERMISSION.ROLES_READ], @@ -67,7 +67,79 @@ const createRoleAuth = async ( return await createRole(create); }; -const assignRoleById = async (id: string, userId: string): Promise => { +const updateRoleById = async ( + id: string, + update: IUpdateRole, +): Promise => { + return await getDB().role.update({ + where: { + id: id, + }, + data: { + ...update, + }, + }); +}; + +const updateRoleByIdAuth = async ( + id: string, + update: IUpdateRole, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const role = await getRoleById(id); + const community = await getCommunityById(role?.communityId ?? ""); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.ROLES_MANAGE], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await updateRoleById(id, update); +}; + +const deleteRoleById = async (id: string): Promise => { + return await getDB().role.delete({ + where: { id: id }, + }); +}; + +const deleteRoleByIdAuth = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const role = await getRoleById(id); + const community = await getCommunityById(role?.communityId ?? ""); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.ROLES_MANAGE], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await deleteRoleById(id); +}; + +const assignRoleById = async ( + id: string, + userId: string, +): Promise => { return await getDB().role.update({ where: { id: id, @@ -86,7 +158,7 @@ const assignRoleByIdAuth = async ( id: string, userId: string, authHeader: string | undefined, -): Promise => { +): Promise => { const authUser = await getUserFromAuth(authHeader); const role = await getRoleById(id); const community = await getCommunityById(role?.communityId ?? ""); @@ -107,7 +179,10 @@ const assignRoleByIdAuth = async ( return await assignRoleById(id, userId); }; -const unassignRoleById = async (id: string, userId: string): Promise => { +const unassignRoleById = async ( + id: string, + userId: string, +): Promise => { return await getDB().role.update({ where: { id: id, @@ -126,7 +201,7 @@ const unassignRoleByIdAuth = async ( id: string, userId: string, authHeader: string | undefined, -): Promise => { +): Promise => { const authUser = await getUserFromAuth(authHeader); const role = await getRoleById(id); const community = await getCommunityById(role?.communityId ?? ""); @@ -152,6 +227,10 @@ export { getRoleByIdAuth, createRole, createRoleAuth, + updateRoleById, + updateRoleByIdAuth, + deleteRoleById, + deleteRoleByIdAuth, assignRoleById, assignRoleByIdAuth, unassignRoleById, diff --git a/src/services/role/types.ts b/src/services/role/types.ts index 86ae5d2..147a989 100644 --- a/src/services/role/types.ts +++ b/src/services/role/types.ts @@ -6,4 +6,9 @@ interface ICreateRole { permissions: PERMISSION[]; } -export { type ICreateRole }; +interface IUpdateRole { + name?: string; + permissions?: PERMISSION[]; +} + +export { type ICreateRole, type IUpdateRole }; diff --git a/src/services/user/types.ts b/src/services/user/types.ts index fee3b16..fe8aed8 100644 --- a/src/services/user/types.ts +++ b/src/services/user/types.ts @@ -1,6 +1,14 @@ +interface ICreateUser { + username: string; + password: string; + email?: string; + description?: string; + admin?: boolean; +} + interface IUpdateUser { email?: string; description?: string; } -export { type IUpdateUser }; +export { type ICreateUser, type IUpdateUser }; diff --git a/src/services/user/user.ts b/src/services/user/user.ts index 2abe0ad..761455f 100644 --- a/src/services/user/user.ts +++ b/src/services/user/user.ts @@ -2,7 +2,7 @@ import type { User, Session } from "../../generated/prisma/client.js"; import { getUserFromAuth, isUserOwnerOrAdmin } from "../auth/helpers.js"; import { getDB } from "../../store/store.js"; import { API_ERROR } from "../../controllers/errors.js"; -import type { IUpdateUser } from "./types.js"; +import type { ICreateUser, IUpdateUser } from "./types.js"; const getUserById = async (id: string): Promise => { return await getDB().user.findUnique({ @@ -28,6 +28,27 @@ const getUserByIdAuth = async ( return user; }; +const createUser = async (create: ICreateUser): Promise => { + return await getDB().user.create({ + data: { + ...create, + }, + }); +}; + +const createUserAuth = async ( + create: ICreateUser, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + + if (!authUser?.admin) { + return API_ERROR.ACCESS_DENIED; + } + + return await createUser(create); +}; + const updateUserById = async ( id: string, update: IUpdateUser, @@ -61,6 +82,30 @@ const updateUserByIdAuth = async ( return await updateUserById(id, update); }; +const deleteUserById = async (id: string): Promise => { + return await getDB().user.delete({ + where: { id: id }, + }); +}; + +const deleteUserByIdAuth = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const user = await getUserById(id); + + if ( + !(await isUserOwnerOrAdmin(authUser, { + user: user, + })) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await deleteUserById(id); +}; + const getUserSessionsById = async (id: string): Promise => { return await getDB().session.findMany({ where: { @@ -91,8 +136,12 @@ const getUserSessionsByIdAuth = async ( export { getUserById, getUserByIdAuth, + createUser, + createUserAuth, updateUserById, updateUserByIdAuth, + deleteUserById, + deleteUserByIdAuth, getUserSessionsById, getUserSessionsByIdAuth, }; diff --git a/tests/1.user.test.js b/tests/1.user.test.js index f9221f3..c4f65be 100644 --- a/tests/1.user.test.js +++ b/tests/1.user.test.js @@ -134,3 +134,43 @@ test("can delete session", async () => { assert.equal(responseSessions2.id, state.userId); assert.equal(responseSessions2.sessions.length, 1); }); + +/* +test("can create user", async () => { + state.newUserName = "New User"; + state.newUserPassword = "2142"; + state.newUserDescription = "This is a New User"; + + const response = await apiPost( + `user`, + { + username: state.newUserName, + password: state.newUserPassword, + description: state.newUserDescription, + }, + state.token, + ); + + assert.equal(validate(response.id), true); + assert.equal(response.username, state.newUserName); + assert.equal(response.description, state.newUserDescription); + assert.equal(response.admin, false); + + state.newUserId = response.id; + + const responseGet = await apiGet(`user/${state.newUserId}`, state.token); + assert.equal(responseGet.username, state.newUserName); +}); + +test("can delete user", async () => { + const responseDelete = await apiDelete( + `user/${state.newUserId}`, + {}, + state.token, + ); + assert.equal(responseDelete.id, state.newUserId); + + const responseGet = await apiGet(`user/${state.newUserId}`, state.token); + assert.equal(responseGet.error, "NOT_FOUND"); +}); +*/ diff --git a/tests/2.community.test.js b/tests/2.community.test.js index b35f41a..2faeef5 100644 --- a/tests/2.community.test.js +++ b/tests/2.community.test.js @@ -44,7 +44,7 @@ test("can create community", async () => { state.sessionId2 = responseLogin2.id; state.token2 = responseLogin2.token; - const createResponse = await apiPost( + const responseCreate = await apiPost( `community`, { name: state.communityName, @@ -52,15 +52,15 @@ test("can create community", async () => { }, state.token1, ); - state.communityId = createResponse.id; - assert.equal(createResponse.name, state.communityName); - assert.equal(createResponse.description, state.communityDescription); - assert.equal(createResponse.ownerId, state.userId1); + state.communityId = responseCreate.id; + assert.equal(responseCreate.name, state.communityName); + assert.equal(responseCreate.description, state.communityDescription); + assert.equal(responseCreate.ownerId, state.userId1); - const getResponse = await apiGet(`community/${state.communityId}`); - assert.equal(getResponse.name, state.communityName); - assert.equal(getResponse.description, state.communityDescription); - assert.equal(getResponse.ownerId, state.userId1); + const responseGet = await apiGet(`community/${state.communityId}`); + assert.equal(responseGet.name, state.communityName); + assert.equal(responseGet.description, state.communityDescription); + assert.equal(responseGet.ownerId, state.userId1); }); test("shouldn't be able to create invite", async () => { @@ -111,14 +111,14 @@ test("can accept invite", async () => { assert.equal(response.communityId, state.communityId); assert.equal(response.communityName, state.communityName); - const getResponse = await apiGet( + const responseGet = await apiGet( `community/${state.communityId}/members`, state.token1, ); - assert.equal(getResponse.id, state.communityId); - assert.equal(getResponse.name, state.communityName); - assert.equal(getResponse.members.length === 2, true); + assert.equal(responseGet.id, state.communityId); + assert.equal(responseGet.name, state.communityName); + assert.equal(responseGet.members.length === 2, true); }); test("can get invite", async () => { @@ -247,6 +247,18 @@ test("can get roles", async () => { assert.equal(response.roles.length === 1, true); }); +test("can get permissions", async () => { + const response = await apiGet( + `role/${state.roleId}/permissions`, + state.token2, + ); + + assert.equal(response.id, state.roleId); + assert.equal(response.name, state.roleName); + assert.equal(response.communityId, state.communityId); + assert.equal(response.permissions.length === 3, true); +}); + test("can unassign role from user", async () => { const response = await apiPost( `role/${state.roleId}/unassign`, @@ -270,3 +282,81 @@ test("shouldn't be able to get channels 2", async () => { assert.equal(response.error, "ACCESS_DENIED"); }); + +test("can update channel", async () => { + state.channelName = "Test Channel Mod"; + + const responsePatch = await apiPatch( + `channel/${state.channelId}`, + { + name: state.channelName, + }, + state.token1, + ); + assert.equal(responsePatch.id, state.channelId); + assert.equal(responsePatch.name, state.channelName); + assert.equal(responsePatch.communityId, state.communityId); + + const responseGet = await apiGet( + `channel/${state.channelId}`, + state.token1, + ); + assert.equal(responseGet.name, state.channelName); +}); + +test("can delete channel", async () => { + const responseDelete = await apiDelete( + `channel/${state.channelId}`, + {}, + state.token1, + ); + assert.equal(responseDelete.id, state.channelId); + + const responseGet = await apiGet( + `channel/${state.channelId}`, + state.token1, + ); + assert.equal(responseGet.error, "ACCESS_DENIED"); +}); + +test("can update role", async () => { + state.roleName = "Test Role Mod"; + + const responsePatch = await apiPatch( + `role/${state.roleId}`, + { + name: state.roleName, + }, + state.token1, + ); + assert.equal(responsePatch.id, state.roleId); + assert.equal(responsePatch.name, state.roleName); + assert.equal(responsePatch.communityId, state.communityId); + + const responseGet = await apiGet(`role/${state.roleId}`, state.token1); + assert.equal(responseGet.name, state.roleName); +}); + +test("can delete role", async () => { + const responseDelete = await apiDelete( + `role/${state.roleId}`, + {}, + state.token1, + ); + assert.equal(responseDelete.id, state.roleId); + + const responseGet = await apiGet(`role/${state.roleId}`, state.token1); + assert.equal(responseGet.error, "ACCESS_DENIED"); +}); + +test("can delete community", async () => { + const responseDelete = await apiDelete( + `community/${state.communityId}`, + {}, + state.token1, + ); + assert.equal(responseDelete.id, state.communityId); + + const responseGet = await apiGet(`community/${state.communityId}`); + assert.equal(responseGet.error, "NOT_FOUND"); +});