From 72d7b22891454ea7de091c7225ddbe0ce0d24fd2443f0bc342361c3c2ac60733 Mon Sep 17 00:00:00 2001 From: aslan Date: Sat, 27 Dec 2025 01:51:42 +0100 Subject: [PATCH] Basic services; Version 0.2.0 --- package.json | 2 +- .../migration.sql | 12 ++ prisma/schema.prisma | 4 +- src/controllers/community/community.ts | 163 +++++++++++++++- src/controllers/community/routes.ts | 4 + src/controllers/community/types.ts | 96 ++++++++++ src/controllers/user/types.ts | 1 + src/controllers/user/user.ts | 1 + src/services/auth/permission.ts | 7 +- src/services/community/community.ts | 174 +++++++++++++++++- src/services/community/types.ts | 30 ++- 11 files changed, 483 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/20251227000613_mandatory_names/migration.sql diff --git a/package.json b/package.json index e1b61b5..9eda4cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tether", - "version": "0.1.0", + "version": "0.2.0", "description": "Communication server using the Nexlink protocol", "repository": { "type": "git", diff --git a/prisma/migrations/20251227000613_mandatory_names/migration.sql b/prisma/migrations/20251227000613_mandatory_names/migration.sql new file mode 100644 index 0000000..5f20851 --- /dev/null +++ b/prisma/migrations/20251227000613_mandatory_names/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Made the column `name` on table `Channel` required. This step will fail if there are existing NULL values in that column. + - Made the column `name` on table `Role` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Channel" ALTER COLUMN "name" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Role" ALTER COLUMN "name" SET NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 34c45c2..fb5efa6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,7 +23,7 @@ model Community { model Channel { id String @id @unique @default(uuid()) - name String? + name String community Community? @relation(fields: [communityId], references: [id]) communityId String? creationDate DateTime @default(now()) @@ -31,7 +31,7 @@ model Channel { model Role { id String @id @unique @default(uuid()) - name String? + name String community Community @relation(fields: [communityId], references: [id]) communityId String users User[] @relation(name: "UsersRolesToUsers") diff --git a/src/controllers/community/community.ts b/src/controllers/community/community.ts index 8ad2732..cf9768f 100644 --- a/src/controllers/community/community.ts +++ b/src/controllers/community/community.ts @@ -7,12 +7,30 @@ import type { IPatchCommunityRequest, IPatchCommunityResponseError, IPatchCommunityResponseSuccess, + IGetMembersParams, + IGetMembersResponseError, + IGetMembersResponseSuccess, + IGetChannelsParams, + IGetChannelsResponseError, + IGetChannelsResponseSuccess, + IGetRolesParams, + IGetRolesResponseError, + IGetRolesResponseSuccess, + IPostCreateInviteParams, + IPostCreateInviteRequest, + IPostCreateInviteResponseError, + IPostCreateInviteResponseSuccess, } from "./types.js"; import { + createInviteAuth, getCommunityById, + getCommunityChannelsByIdAuth, + getCommunityMembersByIdAuth, + getCommunityRolesByIdAuth, updateCommunityByIdAuth, } from "../../services/community/community.js"; import { API_ERROR } from "../errors.js"; +import type { ICreateInvite } from "../../services/community/types.js"; const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as IGetCommunityParams; @@ -66,4 +84,147 @@ const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => { } as IPatchCommunityResponseSuccess; }; -export { getCommunity, patchCommunity }; +const getMembers = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IGetMembersParams; + const authHeader = request.headers["authorization"]; + + const community = await getCommunityById(id); + const members = await getCommunityMembersByIdAuth(id, authHeader); + if (!members || !community) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IGetMembersResponseError; + } + if (members === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IGetMembersResponseError; + } + + return { + id: community.id, + name: community.name, + members: members.map((member) => ({ + id: member.id, + username: member.username, + })), + } as IGetMembersResponseSuccess; +}; + +const getChannels = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IGetChannelsParams; + const authHeader = request.headers["authorization"]; + + const community = await getCommunityById(id); + const channels = await getCommunityChannelsByIdAuth(id, authHeader); + if (!channels || !community) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IGetChannelsResponseError; + } + if (channels === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IGetChannelsResponseError; + } + + return { + id: community.id, + name: community.name, + channels: channels.map((channel) => ({ + id: channel.id, + name: channel.name, + })), + } as IGetChannelsResponseSuccess; +}; + +const getRoles = async (request: FastifyRequest, reply: FastifyReply) => { + const { id } = request.params as IGetRolesParams; + const authHeader = request.headers["authorization"]; + + const community = await getCommunityById(id); + const roles = await getCommunityRolesByIdAuth(id, authHeader); + if (!roles || !community) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IGetRolesResponseError; + } + if (roles === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IGetRolesResponseError; + } + + return { + id: community.id, + name: community.name, + roles: roles.map((role) => ({ + id: role.id, + name: role.name, + })), + } as IGetRolesResponseSuccess; +}; + +const postCreateInvite = async ( + request: FastifyRequest, + reply: FastifyReply, +) => { + const { id } = request.params as IPostCreateInviteParams; + const { creatorId, totalInvites, expirationDate } = + request.body as IPostCreateInviteRequest; + const authHeader = request.headers["authorization"]; + + const createInviteData = { + creatorId: creatorId, + } as ICreateInvite; + if (totalInvites) { + createInviteData.totalInvites = totalInvites; + createInviteData.remainingInvites = totalInvites; + } + if (expirationDate) { + createInviteData.expirationDate = new Date(expirationDate); + } + + const community = await getCommunityById(id); + const invite = await createInviteAuth(id, createInviteData, authHeader); + if (!invite || !community) { + reply.status(404); + return { + id: id, + error: API_ERROR.NOT_FOUND, + } as IPostCreateInviteResponseError; + } + if (invite === API_ERROR.ACCESS_DENIED) { + reply.status(403); + return { + id: id, + error: API_ERROR.ACCESS_DENIED, + } as IPostCreateInviteResponseError; + } + + return { + id: community.id, + inviteId: invite.id, + } as IPostCreateInviteResponseSuccess; +}; + +export { + getCommunity, + patchCommunity, + getMembers, + getChannels, + getRoles, + postCreateInvite, +}; diff --git a/src/controllers/community/routes.ts b/src/controllers/community/routes.ts index f5ee5fc..975e8a8 100644 --- a/src/controllers/community/routes.ts +++ b/src/controllers/community/routes.ts @@ -4,6 +4,10 @@ import * as controller from "./community.js"; const communityRoutes = async (fastify: FastifyInstance) => { fastify.get(`/:id`, controller.getCommunity); fastify.patch(`/:id`, controller.patchCommunity); + fastify.get(`/:id/members`, controller.getMembers); + fastify.get(`/:id/channels`, controller.getChannels); + fastify.get(`/:id/roles`, controller.getRoles); + fastify.post(`/:id/invite`, controller.postCreateInvite); }; export { communityRoutes }; diff --git a/src/controllers/community/types.ts b/src/controllers/community/types.ts index 462c9f8..9d7703a 100644 --- a/src/controllers/community/types.ts +++ b/src/controllers/community/types.ts @@ -34,6 +34,86 @@ interface IPatchCommunityResponseSuccess { description: string; } +interface IGetMembersParams { + id: string; +} + +interface IGetMembersResponseError { + id: string; + error: string; +} + +interface IGetMembersResponseSuccess { + id: string; + name: string; + members: IGetMembersResponseMembers[]; +} + +interface IGetMembersResponseMembers { + id: string; + username: string; +} + +interface IGetChannelsParams { + id: string; +} + +interface IGetChannelsResponseError { + id: string; + error: string; +} + +interface IGetChannelsResponseSuccess { + id: string; + name: string; + channels: IGetChannelsResponseChannels[]; +} + +interface IGetChannelsResponseChannels { + id: string; + name: string; +} + +interface IGetRolesParams { + id: string; +} + +interface IGetRolesResponseError { + id: string; + error: string; +} + +interface IGetRolesResponseSuccess { + id: string; + name: string; + roles: IGetRolesResponseRoles[]; +} + +interface IGetRolesResponseRoles { + id: string; + name: string; +} + +interface IPostCreateInviteParams { + id: string; +} + +interface IPostCreateInviteRequest { + creatorId: string; + totalInvites?: number; + expirationDate?: number; +} + +interface IPostCreateInviteResponseError { + id: string; + error: string; +} + +interface IPostCreateInviteResponseSuccess { + id: string; + inviteId: string; +} + export { type IGetCommunityParams, type IGetCommunityResponseError, @@ -42,4 +122,20 @@ export { type IPatchCommunityRequest, type IPatchCommunityResponseError, type IPatchCommunityResponseSuccess, + type IGetMembersParams, + type IGetMembersResponseError, + type IGetMembersResponseSuccess, + type IGetMembersResponseMembers, + type IGetChannelsParams, + type IGetChannelsResponseError, + type IGetChannelsResponseSuccess, + type IGetChannelsResponseChannels, + type IGetRolesParams, + type IGetRolesResponseError, + type IGetRolesResponseSuccess, + type IGetRolesResponseRoles, + type IPostCreateInviteParams, + type IPostCreateInviteRequest, + type IPostCreateInviteResponseError, + type IPostCreateInviteResponseSuccess, }; diff --git a/src/controllers/user/types.ts b/src/controllers/user/types.ts index c30d26b..44f1246 100644 --- a/src/controllers/user/types.ts +++ b/src/controllers/user/types.ts @@ -47,6 +47,7 @@ interface IGetSessionsResponseError { } interface IGetSessionsResponseSuccess { + id: string; sessions: IGetSessionsResponseSession[]; } diff --git a/src/controllers/user/user.ts b/src/controllers/user/user.ts index e677d0a..ea4bbfc 100644 --- a/src/controllers/user/user.ts +++ b/src/controllers/user/user.ts @@ -98,6 +98,7 @@ const getSessions = async (request: FastifyRequest, reply: FastifyReply) => { } return { + id: id, sessions: sessions.map((session) => ({ id: session.id, userId: session.userId, diff --git a/src/services/auth/permission.ts b/src/services/auth/permission.ts index fb1f896..0d9d217 100644 --- a/src/services/auth/permission.ts +++ b/src/services/auth/permission.ts @@ -1,13 +1,14 @@ enum PERMISSION { COMMUNITY_MANAGE = "COMMUNITY_MANAGE", - CHANNELS_MANAGE = "CHANNELS_MANAGE", CHANNELS_READ = "CHANNELS_READ", + CHANNELS_MANAGE = "CHANNELS_MANAGE", ROLES_READ = "ROLES_READ", INVITES_CREATE = "INVITES_CREATE", INVITES_DELETE = "INVITES_DELETE", ROLES_MANAGE = "ROLES_MANAGE", - USERS_KICK = "USERS_KICK", - USERS_BAN = "USERS_BAN", + MEMBERS_READ = "MEMBERS_READ", + MEMBERS_KICK = "MEMBERS_KICK", + MEMBERS_BAN = "MEMBERS_BAN", } export { PERMISSION }; diff --git a/src/services/community/community.ts b/src/services/community/community.ts index 6bddbae..dadc94a 100644 --- a/src/services/community/community.ts +++ b/src/services/community/community.ts @@ -1,9 +1,16 @@ import { API_ERROR } from "../../controllers/errors.js"; -import type { Community } from "../../generated/prisma/client.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 { PERMISSION } from "../auth/permission.js"; -import type { IUpdateCommunity } from "./types.js"; +import { getInviteById } from "../invite/invite.js"; +import type { + ICommunityChannel, + ICommunityMember, + ICommunityRole, + ICreateInvite, + IUpdateCommunity, +} from "./types.js"; const getCommunityById = async (id: string): Promise => { return await getDB().community.findUnique({ @@ -49,4 +56,165 @@ const updateCommunityByIdAuth = async ( return await updateCommunityById(id, update); }; -export { getCommunityById, updateCommunityById, updateCommunityByIdAuth }; +const getCommunityMembersById = async ( + id: string, +): Promise => { + return await getDB().user.findMany({ + where: { + communities: { + some: { + id: id, + }, + }, + }, + select: { + id: true, + username: true, + }, + }); +}; + +const getCommunityMembersByIdAuth = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(id); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.MEMBERS_READ], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await getCommunityMembersById(id); +}; + +const getCommunityChannelsById = async ( + id: string, +): Promise => { + return await getDB().channel.findMany({ + where: { + communityId: id, + }, + select: { + id: true, + name: true, + }, + }); +}; + +const getCommunityChannelsByIdAuth = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(id); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.CHANNELS_READ], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await getCommunityChannelsById(id); +}; + +const getCommunityRolesById = async (id: string): Promise => { + return await getDB().role.findMany({ + where: { + communityId: id, + }, + select: { + id: true, + name: true, + }, + }); +}; + +const getCommunityRolesByIdAuth = async ( + id: string, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(id); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.ROLES_READ], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await getCommunityRolesById(id); +}; + +const createInvite = async ( + id: string, + createInviteData: ICreateInvite, +): Promise => { + return await getDB().invite.create({ + data: { + ...createInviteData, + communityId: id, + }, + }); +}; + +const createInviteAuth = async ( + id: string, + createInviteData: ICreateInvite, + authHeader: string | undefined, +): Promise => { + const authUser = await getUserFromAuth(authHeader); + const community = await getCommunityById(id); + + if ( + !(await isUserAllowed( + authUser, + { + community: community, + }, + community, + [PERMISSION.INVITES_CREATE], + )) + ) { + return API_ERROR.ACCESS_DENIED; + } + + return await createInvite(id, createInviteData); +}; + +export { + getCommunityById, + updateCommunityById, + updateCommunityByIdAuth, + getCommunityMembersById, + getCommunityMembersByIdAuth, + getCommunityChannelsById, + getCommunityChannelsByIdAuth, + getCommunityRolesById, + getCommunityRolesByIdAuth, + createInvite, + createInviteAuth, +}; diff --git a/src/services/community/types.ts b/src/services/community/types.ts index 4ad8446..fa5c501 100644 --- a/src/services/community/types.ts +++ b/src/services/community/types.ts @@ -3,4 +3,32 @@ interface IUpdateCommunity { description?: string; } -export { type IUpdateCommunity }; +interface ICommunityMember { + id: string; + username: string; +} + +interface ICommunityChannel { + id: string; + name: string; +} + +interface ICommunityRole { + id: string; + name: string; +} + +interface ICreateInvite { + creatorId: string; + totalInvites?: number; + remainingInvites?: number; + expirationDate?: Date; +} + +export { + type IUpdateCommunity, + type ICommunityMember, + type ICommunityChannel, + type ICommunityRole, + type ICreateInvite, +};