New endpoints; New tests; Version 0.3.0

This commit is contained in:
Aslan 2025-12-28 04:29:42 +01:00
parent 4bc3be87b4
commit 1f3a94cf38
27 changed files with 551 additions and 72 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "tether", "name": "tether",
"version": "0.2.0", "version": "0.3.0",
"description": "Communication server using the Nexlink protocol", "description": "Communication server using the Nexlink protocol",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -0,0 +1,14 @@
/*
Warnings:
- Made the column `communityId` on table `Channel` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Channel" DROP CONSTRAINT "Channel_communityId_fkey";
-- AlterTable
ALTER TABLE "Channel" ALTER COLUMN "communityId" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "Channel" ADD CONSTRAINT "Channel_communityId_fkey" FOREIGN KEY ("communityId") REFERENCES "Community"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -22,11 +22,11 @@ model Community {
} }
model Channel { model Channel {
id String @id @unique @default(uuid()) id String @id @unique @default(uuid())
name String name String
community Community? @relation(fields: [communityId], references: [id]) community Community @relation(fields: [communityId], references: [id])
communityId String? communityId String
creationDate DateTime @default(now()) creationDate DateTime @default(now())
} }
model Role { model Role {

View file

@ -3,8 +3,14 @@ import type {
IGetChannelParams, IGetChannelParams,
IGetChannelResponseError, IGetChannelResponseError,
IGetChannelResponseSuccess, IGetChannelResponseSuccess,
IPostCreateChannelRequest,
IPostCreateChannelResponseError,
IPostCreateChannelResponseSuccess,
} from "./types.js"; } from "./types.js";
import { getChannelByIdAuth } from "../../services/channel/channel.js"; import {
createChannelAuth,
getChannelByIdAuth,
} from "../../services/channel/channel.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
const getChannel = async (request: FastifyRequest, reply: FastifyReply) => { const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
@ -35,4 +41,26 @@ const getChannel = async (request: FastifyRequest, reply: FastifyReply) => {
} as IGetChannelResponseSuccess; } as IGetChannelResponseSuccess;
}; };
export { getChannel }; const postCreateChannel = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const createChannelRequest = request.body as IPostCreateChannelRequest;
const authHeader = request.headers["authorization"];
const channel = await createChannelAuth(createChannelRequest, authHeader);
if (channel === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
error: API_ERROR.ACCESS_DENIED,
} as IPostCreateChannelResponseError;
}
return {
id: channel.id,
name: channel.name,
communityId: channel.communityId,
} as IPostCreateChannelResponseSuccess;
};
export { getChannel, postCreateChannel };

View file

@ -3,6 +3,7 @@ import * as controller from "./channel.js";
const channelRoutes = async (fastify: FastifyInstance) => { const channelRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getChannel); fastify.get(`/:id`, controller.getChannel);
fastify.post(`/`, controller.postCreateChannel);
}; };
export { channelRoutes }; export { channelRoutes };

View file

@ -16,8 +16,27 @@ interface IGetChannelResponseSuccess {
creationDate: number; creationDate: number;
} }
interface IPostCreateChannelRequest {
name: string;
communityId: string;
}
interface IPostCreateChannelResponseError {
id: string;
error: API_ERROR;
}
interface IPostCreateChannelResponseSuccess {
id: string;
name: string;
communityId: string;
}
export { export {
type IGetChannelParams, type IGetChannelParams,
type IGetChannelResponseError, type IGetChannelResponseError,
type IGetChannelResponseSuccess, type IGetChannelResponseSuccess,
type IPostCreateChannelRequest,
type IPostCreateChannelResponseError,
type IPostCreateChannelResponseSuccess,
}; };

View file

@ -3,6 +3,9 @@ import type {
IGetCommunityParams, IGetCommunityParams,
IGetCommunityResponseError, IGetCommunityResponseError,
IGetCommunityResponseSuccess, IGetCommunityResponseSuccess,
IPostCreateCommunityRequest,
IPostCreateCommunityResponseError,
IPostCreateCommunityResponseSuccess,
IPatchCommunityParams, IPatchCommunityParams,
IPatchCommunityRequest, IPatchCommunityRequest,
IPatchCommunityResponseError, IPatchCommunityResponseError,
@ -22,12 +25,13 @@ import type {
IPostCreateInviteResponseSuccess, IPostCreateInviteResponseSuccess,
} from "./types.js"; } from "./types.js";
import { import {
createInviteAuth,
getCommunityById, getCommunityById,
createCommunityAuth,
updateCommunityByIdAuth,
getCommunityChannelsByIdAuth, getCommunityChannelsByIdAuth,
getCommunityMembersByIdAuth, getCommunityMembersByIdAuth,
getCommunityRolesByIdAuth, getCommunityRolesByIdAuth,
updateCommunityByIdAuth, createInviteAuth,
} from "../../services/community/community.js"; } from "../../services/community/community.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
import type { ICreateInvite } from "../../services/community/types.js"; import type { ICreateInvite } from "../../services/community/types.js";
@ -48,10 +52,37 @@ const getCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
id: community.id, id: community.id,
name: community.name, name: community.name,
description: community.description, description: community.description,
ownerId: community.ownerId,
creationDate: community.creationDate.getTime(), creationDate: community.creationDate.getTime(),
} as IGetCommunityResponseSuccess; } as IGetCommunityResponseSuccess;
}; };
const postCreateCommunity = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const createCommunityRequest = request.body as IPostCreateCommunityRequest;
const authHeader = request.headers["authorization"];
const community = await createCommunityAuth(
createCommunityRequest,
authHeader,
);
if (community === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
error: API_ERROR.ACCESS_DENIED,
} as IPostCreateCommunityResponseError;
}
return {
id: community.id,
name: community.name,
description: community.description,
ownerId: community.ownerId,
} as IPostCreateCommunityResponseSuccess;
};
const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => { const patchCommunity = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IPatchCommunityParams; const { id } = request.params as IPatchCommunityParams;
const patchCommunityRequest = request.body as IPatchCommunityRequest; const patchCommunityRequest = request.body as IPatchCommunityRequest;
@ -182,13 +213,11 @@ const postCreateInvite = async (
reply: FastifyReply, reply: FastifyReply,
) => { ) => {
const { id } = request.params as IPostCreateInviteParams; const { id } = request.params as IPostCreateInviteParams;
const { creatorId, totalInvites, expirationDate } = const { totalInvites, expirationDate } =
request.body as IPostCreateInviteRequest; request.body as IPostCreateInviteRequest;
const authHeader = request.headers["authorization"]; const authHeader = request.headers["authorization"];
const createInviteData = { const createInviteData = {} as ICreateInvite;
creatorId: creatorId,
} as ICreateInvite;
if (totalInvites) { if (totalInvites) {
createInviteData.totalInvites = totalInvites; createInviteData.totalInvites = totalInvites;
createInviteData.remainingInvites = totalInvites; createInviteData.remainingInvites = totalInvites;
@ -222,6 +251,7 @@ const postCreateInvite = async (
export { export {
getCommunity, getCommunity,
postCreateCommunity,
patchCommunity, patchCommunity,
getMembers, getMembers,
getChannels, getChannels,

View file

@ -3,6 +3,7 @@ import * as controller from "./community.js";
const communityRoutes = async (fastify: FastifyInstance) => { const communityRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getCommunity); fastify.get(`/:id`, controller.getCommunity);
fastify.post(`/`, controller.postCreateCommunity);
fastify.patch(`/:id`, controller.patchCommunity); fastify.patch(`/:id`, controller.patchCommunity);
fastify.get(`/:id/members`, controller.getMembers); fastify.get(`/:id/members`, controller.getMembers);
fastify.get(`/:id/channels`, controller.getChannels); fastify.get(`/:id/channels`, controller.getChannels);

View file

@ -13,9 +13,27 @@ interface IGetCommunityResponseSuccess {
id: string; id: string;
name: string; name: string;
description: string; description: string;
ownerId: string;
creationDate: number; creationDate: number;
} }
interface IPostCreateCommunityRequest {
name: string;
description?: string;
}
interface IPostCreateCommunityResponseError {
id: string;
error: API_ERROR;
}
interface IPostCreateCommunityResponseSuccess {
id: string;
name: string;
description: string;
ownerId: string;
}
interface IPatchCommunityParams { interface IPatchCommunityParams {
id: string; id: string;
} }
@ -101,7 +119,6 @@ interface IPostCreateInviteParams {
} }
interface IPostCreateInviteRequest { interface IPostCreateInviteRequest {
creatorId: string;
totalInvites?: number; totalInvites?: number;
expirationDate?: number; expirationDate?: number;
} }
@ -120,6 +137,9 @@ export {
type IGetCommunityParams, type IGetCommunityParams,
type IGetCommunityResponseError, type IGetCommunityResponseError,
type IGetCommunityResponseSuccess, type IGetCommunityResponseSuccess,
type IPostCreateCommunityRequest,
type IPostCreateCommunityResponseError,
type IPostCreateCommunityResponseSuccess,
type IPatchCommunityParams, type IPatchCommunityParams,
type IPatchCommunityRequest, type IPatchCommunityRequest,
type IPatchCommunityResponseError, type IPatchCommunityResponseError,

View file

@ -7,7 +7,6 @@ import type {
IGetInviteResponseError, IGetInviteResponseError,
IGetInviteResponseSuccess, IGetInviteResponseSuccess,
IPostAcceptInviteParams, IPostAcceptInviteParams,
IPostAcceptInviteRequest,
IPostAcceptDeleteInviteResponseError, IPostAcceptDeleteInviteResponseError,
IPostAcceptDeleteInviteResponseSuccess, IPostAcceptDeleteInviteResponseSuccess,
} from "./types.js"; } from "./types.js";
@ -19,7 +18,7 @@ import {
isInviteValid, isInviteValid,
} from "../../services/invite/invite.js"; } from "../../services/invite/invite.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
import { getUserById } from "../../services/user/user.js"; import { getUserFromAuth } from "../../services/auth/helpers.js";
const getInvite = async (request: FastifyRequest, reply: FastifyReply) => { const getInvite = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as IGetInviteParams; const { id } = request.params as IGetInviteParams;
@ -39,6 +38,7 @@ const getInvite = async (request: FastifyRequest, reply: FastifyReply) => {
valid: isInviteValid(invite), valid: isInviteValid(invite),
unlimitedInvites: hasUnlimitedInvites(invite), unlimitedInvites: hasUnlimitedInvites(invite),
hasExpiration: invite.expirationDate != null, hasExpiration: invite.expirationDate != null,
totalInvites: invite.totalInvites,
remainingInvites: invite.remainingInvites, remainingInvites: invite.remainingInvites,
creationDate: invite.creationDate.getTime(), creationDate: invite.creationDate.getTime(),
expirationDate: invite.expirationDate?.getTime() ?? 0, expirationDate: invite.expirationDate?.getTime() ?? 0,
@ -71,21 +71,19 @@ const deleteInvite = async (request: FastifyRequest, reply: FastifyReply) => {
} as IDeleteInviteResponseSuccess; } as IDeleteInviteResponseSuccess;
}; };
const postAcceptInvite = async ( const getAcceptInvite = async (
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply, reply: FastifyReply,
) => { ) => {
const { id } = request.params as IPostAcceptInviteParams; const { id } = request.params as IPostAcceptInviteParams;
const { userId } = request.body as IPostAcceptInviteRequest;
const authHeader = request.headers["authorization"]; const authHeader = request.headers["authorization"];
const community = await acceptInviteByIdAuth(id, authHeader); const community = await acceptInviteByIdAuth(id, authHeader);
const user = await getUserById(id); const user = await getUserFromAuth(authHeader);
if (!community || !user) { if (!community || !user) {
reply.status(404); reply.status(404);
return { return {
id: id, id: id,
userId: userId,
error: API_ERROR.NOT_FOUND, error: API_ERROR.NOT_FOUND,
} as IPostAcceptDeleteInviteResponseError; } as IPostAcceptDeleteInviteResponseError;
} }
@ -93,7 +91,6 @@ const postAcceptInvite = async (
reply.status(403); reply.status(403);
return { return {
id: id, id: id,
userId: userId,
error: API_ERROR.ACCESS_DENIED, error: API_ERROR.ACCESS_DENIED,
} as IPostAcceptDeleteInviteResponseError; } as IPostAcceptDeleteInviteResponseError;
} }
@ -107,4 +104,4 @@ const postAcceptInvite = async (
} as IPostAcceptDeleteInviteResponseSuccess; } as IPostAcceptDeleteInviteResponseSuccess;
}; };
export { getInvite, deleteInvite, postAcceptInvite }; export { getInvite, deleteInvite, getAcceptInvite };

View file

@ -4,7 +4,7 @@ import * as controller from "./index.js";
const inviteRoutes = async (fastify: FastifyInstance) => { const inviteRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getInvite); fastify.get(`/:id`, controller.getInvite);
fastify.delete(`/:id`, controller.deleteInvite); fastify.delete(`/:id`, controller.deleteInvite);
fastify.post(`/:id/accept`, controller.postAcceptInvite); fastify.get(`/:id/accept`, controller.getAcceptInvite);
}; };
export { inviteRoutes }; export { inviteRoutes };

View file

@ -15,6 +15,7 @@ interface IGetInviteResponseSuccess {
valid: boolean; valid: boolean;
unlimitedInvites: boolean; unlimitedInvites: boolean;
hasExpiration: boolean; hasExpiration: boolean;
totalInvites: number;
remainingInvites: number; remainingInvites: number;
creationDate: number; creationDate: number;
expirationDate: number; expirationDate: number;
@ -38,13 +39,8 @@ interface IPostAcceptInviteParams {
id: string; id: string;
} }
interface IPostAcceptInviteRequest {
userId: string;
}
interface IPostAcceptDeleteInviteResponseError { interface IPostAcceptDeleteInviteResponseError {
id: string; id: string;
userId: string;
error: API_ERROR; error: API_ERROR;
} }
@ -64,7 +60,6 @@ export {
type IDeleteInviteResponseError, type IDeleteInviteResponseError,
type IDeleteInviteResponseSuccess, type IDeleteInviteResponseSuccess,
type IPostAcceptInviteParams, type IPostAcceptInviteParams,
type IPostAcceptInviteRequest,
type IPostAcceptDeleteInviteResponseError, type IPostAcceptDeleteInviteResponseError,
type IPostAcceptDeleteInviteResponseSuccess, type IPostAcceptDeleteInviteResponseSuccess,
}; };

View file

@ -3,8 +3,11 @@ import type {
IGetRoleParams, IGetRoleParams,
IGetRoleResponseError, IGetRoleResponseError,
IGetRoleResponseSuccess, IGetRoleResponseSuccess,
IPostCreateRoleRequest,
IPostCreateRoleResponseError,
IPostCreateRoleResponseSuccess,
} from "./types.js"; } from "./types.js";
import { getRoleByIdAuth } from "../../services/role/role.js"; import { createRoleAuth, getRoleByIdAuth } from "../../services/role/role.js";
import { API_ERROR } from "../errors.js"; import { API_ERROR } from "../errors.js";
const getRole = async (request: FastifyRequest, reply: FastifyReply) => { const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
@ -35,4 +38,23 @@ const getRole = async (request: FastifyRequest, reply: FastifyReply) => {
} as IGetRoleResponseSuccess; } as IGetRoleResponseSuccess;
}; };
export { getRole }; const postCreateRole = async (request: FastifyRequest, reply: FastifyReply) => {
const createRoleRequest = request.body as IPostCreateRoleRequest;
const authHeader = request.headers["authorization"];
const role = await createRoleAuth(createRoleRequest, authHeader);
if (role === API_ERROR.ACCESS_DENIED) {
reply.status(403);
return {
error: API_ERROR.ACCESS_DENIED,
} as IPostCreateRoleResponseError;
}
return {
id: role.id,
name: role.name,
communityId: role.communityId,
} as IPostCreateRoleResponseSuccess;
};
export { getRole, postCreateRole };

View file

@ -3,6 +3,7 @@ import * as controller from "./role.js";
const roleRoutes = async (fastify: FastifyInstance) => { const roleRoutes = async (fastify: FastifyInstance) => {
fastify.get(`/:id`, controller.getRole); fastify.get(`/:id`, controller.getRole);
fastify.post(`/`, controller.postCreateRole);
}; };
export { roleRoutes }; export { roleRoutes };

View file

@ -1,3 +1,4 @@
import type { PERMISSION } from "../../services/auth/permission.js";
import type { API_ERROR } from "../errors.js"; import type { API_ERROR } from "../errors.js";
interface IGetRoleParams { interface IGetRoleParams {
@ -16,8 +17,28 @@ interface IGetRoleResponseSuccess {
creationDate: number; creationDate: number;
} }
interface IPostCreateRoleRequest {
name: string;
communityId: string;
permissions: PERMISSION[];
}
interface IPostCreateRoleResponseError {
id: string;
error: API_ERROR;
}
interface IPostCreateRoleResponseSuccess {
id: string;
name: string;
communityId: string;
}
export { export {
type IGetRoleParams, type IGetRoleParams,
type IGetRoleResponseError, type IGetRoleResponseError,
type IGetRoleResponseSuccess, type IGetRoleResponseSuccess,
type IPostCreateRoleRequest,
type IPostCreateRoleResponseError,
type IPostCreateRoleResponseSuccess,
}; };

View file

@ -4,6 +4,7 @@ import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js"; import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js"; import { PERMISSION } from "../auth/permission.js";
import { getCommunityById } from "../community/community.js"; import { getCommunityById } from "../community/community.js";
import type { ICreateChannel } from "./types.js";
const getChannelById = async (id: string): Promise<Channel | null> => { const getChannelById = async (id: string): Promise<Channel | null> => {
return await getDB().channel.findUnique({ return await getDB().channel.findUnique({
@ -35,4 +36,35 @@ const getChannelByIdAuth = async (
return channel; return channel;
}; };
export { getChannelById, getChannelByIdAuth }; const createChannel = async (create: ICreateChannel): Promise<Channel> => {
return await getDB().channel.create({
data: {
...create,
},
});
};
const createChannelAuth = async (
create: ICreateChannel,
authHeader: string | undefined,
): Promise<Channel | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const community = await getCommunityById(create.communityId);
if (
!(await isUserAllowed(
authUser,
{
community: community,
},
community,
[PERMISSION.CHANNELS_MANAGE],
))
) {
return API_ERROR.ACCESS_DENIED;
}
return await createChannel(create);
};
export { getChannelById, getChannelByIdAuth, createChannel, createChannelAuth };

View file

@ -1 +1,2 @@
export * from "./channel.js"; export * from "./channel.js";
export * from "./types.js";

View file

@ -0,0 +1,6 @@
interface ICreateChannel {
name: string;
communityId: string;
}
export { type ICreateChannel };

View file

@ -3,8 +3,8 @@ import type { Community, Invite, User } from "../../generated/prisma/client.js";
import { getDB } from "../../store/store.js"; import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js"; import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js"; import { PERMISSION } from "../auth/permission.js";
import { getInviteById } from "../invite/invite.js";
import type { import type {
ICreateCommunity,
ICommunityChannel, ICommunityChannel,
ICommunityMember, ICommunityMember,
ICommunityRole, ICommunityRole,
@ -18,6 +18,36 @@ const getCommunityById = async (id: string): Promise<Community | null> => {
}); });
}; };
const createCommunity = async (
ownerId: string,
create: ICreateCommunity,
): Promise<Community> => {
return await getDB().community.create({
data: {
ownerId: ownerId,
...create,
members: {
connect: {
id: ownerId,
},
},
},
});
};
const createCommunityAuth = async (
create: ICreateCommunity,
authHeader: string | undefined,
): Promise<Community | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
if (!authUser) {
return API_ERROR.ACCESS_DENIED;
}
return await createCommunity(authUser.id, create);
};
const updateCommunityById = async ( const updateCommunityById = async (
id: string, id: string,
update: IUpdateCommunity, update: IUpdateCommunity,
@ -171,11 +201,13 @@ const getCommunityRolesByIdAuth = async (
const createInvite = async ( const createInvite = async (
id: string, id: string,
creatorId: string,
createInviteData: ICreateInvite, createInviteData: ICreateInvite,
): Promise<Invite> => { ): Promise<Invite> => {
return await getDB().invite.create({ return await getDB().invite.create({
data: { data: {
...createInviteData, ...createInviteData,
creatorId: creatorId,
communityId: id, communityId: id,
}, },
}); });
@ -197,16 +229,19 @@ const createInviteAuth = async (
}, },
community, community,
[PERMISSION.INVITES_CREATE], [PERMISSION.INVITES_CREATE],
)) )) ||
!authUser
) { ) {
return API_ERROR.ACCESS_DENIED; return API_ERROR.ACCESS_DENIED;
} }
return await createInvite(id, createInviteData); return await createInvite(id, authUser.id, createInviteData);
}; };
export { export {
getCommunityById, getCommunityById,
createCommunity,
createCommunityAuth,
updateCommunityById, updateCommunityById,
updateCommunityByIdAuth, updateCommunityByIdAuth,
getCommunityMembersById, getCommunityMembersById,

View file

@ -1,3 +1,8 @@
interface ICreateCommunity {
name: string;
description?: string;
}
interface IUpdateCommunity { interface IUpdateCommunity {
name?: string; name?: string;
description?: string; description?: string;
@ -19,13 +24,13 @@ interface ICommunityRole {
} }
interface ICreateInvite { interface ICreateInvite {
creatorId: string;
totalInvites?: number; totalInvites?: number;
remainingInvites?: number; remainingInvites?: number;
expirationDate?: Date; expirationDate?: Date;
} }
export { export {
type ICreateCommunity,
type IUpdateCommunity, type IUpdateCommunity,
type ICommunityMember, type ICommunityMember,
type ICommunityChannel, type ICommunityChannel,

View file

@ -3,8 +3,6 @@ import {
getUserFromAuth, getUserFromAuth,
isUserAllowed, isUserAllowed,
isUserInCommunity, isUserInCommunity,
isUserOwnerOrAdmin,
userHasPermissions,
} from "../auth/helpers.js"; } from "../auth/helpers.js";
import { getDB } from "../../store/store.js"; import { getDB } from "../../store/store.js";
import { getCommunityById } from "../community/community.js"; import { getCommunityById } from "../community/community.js";

View file

@ -1 +1,2 @@
export * from "./role.js"; export * from "./role.js";
export * from "./types.js";

View file

@ -4,6 +4,7 @@ import { getDB } from "../../store/store.js";
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js"; import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
import { PERMISSION } from "../auth/permission.js"; import { PERMISSION } from "../auth/permission.js";
import { getCommunityById } from "../community/community.js"; import { getCommunityById } from "../community/community.js";
import type { ICreateRole } from "./types.js";
const getRoleById = async (id: string): Promise<Role | null> => { const getRoleById = async (id: string): Promise<Role | null> => {
return await getDB().role.findUnique({ return await getDB().role.findUnique({
@ -35,4 +36,35 @@ const getRoleByIdAuth = async (
return role; return role;
}; };
export { getRoleById, getRoleByIdAuth }; const createRole = async (create: ICreateRole): Promise<Role> => {
return await getDB().role.create({
data: {
...create,
},
});
};
const createRoleAuth = async (
create: ICreateRole,
authHeader: string | undefined,
): Promise<Role | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
const community = await getCommunityById(create.communityId);
if (
!(await isUserAllowed(
authUser,
{
community: community,
},
community,
[PERMISSION.ROLES_MANAGE],
))
) {
return API_ERROR.ACCESS_DENIED;
}
return await createRole(create);
};
export { getRoleById, getRoleByIdAuth, createRole, createRoleAuth };

View file

@ -0,0 +1,9 @@
import type { PERMISSION } from "../auth/permission.js";
interface ICreateRole {
name: string;
communityId: string;
permissions: PERMISSION[];
}
export { type ICreateRole };

244
tests/2.community.test.js Normal file
View file

@ -0,0 +1,244 @@
import assert from "node:assert";
import { test } from "node:test";
import { validate } from "uuid";
import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js";
const state = {};
test("can create community", async () => {
state.communityName = "testCommunity";
state.communityDescription = "this is a test community";
state.username1 = "testuser1";
state.password1 = "8556";
state.email1 = "testuser1@test.test";
state.username2 = "testuser2";
state.password2 = "8556";
state.email2 = "testuser2@test.test";
const responseRegister1 = await apiPost(`auth/register`, {
username: state.username1,
password: state.password1,
email: state.email1,
});
state.userId1 = responseRegister1.id;
const responseRegister2 = await apiPost(`auth/register`, {
username: state.username2,
password: state.password2,
email: state.email2,
});
state.userId2 = responseRegister2.id;
const responseLogin1 = await apiPost(`auth/login`, {
username: state.username1,
password: state.password1,
});
state.sessionId1 = responseLogin1.id;
state.token1 = responseLogin1.token;
const responseLogin2 = await apiPost(`auth/login`, {
username: state.username2,
password: state.password2,
});
state.sessionId2 = responseLogin2.id;
state.token2 = responseLogin2.token;
const createResponse = await apiPost(
`community`,
{
name: state.communityName,
description: state.communityDescription,
},
state.token1,
);
state.communityId = createResponse.id;
assert.equal(createResponse.name, state.communityName);
assert.equal(createResponse.description, state.communityDescription);
assert.equal(createResponse.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);
});
test("shouldn't be able to create invite", async () => {
const response = await apiPost(
`community/${state.communityId}/invite`,
{
totalInvites: 1,
},
state.token2,
);
assert.equal(response.error, "ACCESS_DENIED");
});
test("can create invite", async () => {
const response = await apiPost(
`community/${state.communityId}/invite`,
{
totalInvites: 1,
},
state.token1,
);
assert.equal(response.id, state.communityId);
assert.equal(validate(response.inviteId), true);
state.inviteId = response.inviteId;
});
test("shouldn't be able to accept invite", async () => {
const response = await apiGet(
`invite/${state.inviteId}/accept`,
state.token1,
);
assert.equal(response.error, "ACCESS_DENIED");
});
test("can accept invite", async () => {
const response = await apiGet(
`invite/${state.inviteId}/accept`,
state.token2,
);
assert.equal(response.id, state.inviteId);
assert.equal(response.userId, state.userId2);
assert.equal(response.userName, state.username2);
assert.equal(response.communityId, state.communityId);
assert.equal(response.communityName, state.communityName);
const getResponse = 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);
});
test("can get invite", async () => {
const response = await apiGet(`invite/${state.inviteId}`);
assert.equal(response.id, state.inviteId);
assert.equal(response.communityId, state.communityId);
assert.equal(response.valid, false);
assert.equal(response.unlimitedInvites, false);
assert.equal(response.hasExpiration, false);
assert.equal(response.totalInvites, 1);
assert.equal(response.remainingInvites, 0);
});
test("shouldn't be able to get members", async () => {
const response = await apiGet(
`community/${state.communityId}/members`,
state.token2,
);
assert.equal(response.error, "ACCESS_DENIED");
});
test("shouldn't be able to get channels", async () => {
const response = await apiGet(
`community/${state.communityId}/channels`,
state.token2,
);
assert.equal(response.error, "ACCESS_DENIED");
});
test("shouldn't be able to get roles", async () => {
const response = await apiGet(
`community/${state.communityId}/roles`,
state.token2,
);
assert.equal(response.error, "ACCESS_DENIED");
});
test("can create channel", async () => {
state.channelName = "Test Channel";
const response = await apiPost(
`channel`,
{
name: state.channelName,
communityId: state.communityId,
},
state.token1,
);
assert.equal(validate(response.id), true);
assert.equal(response.name, state.channelName);
assert.equal(response.communityId, state.communityId);
state.channelId = response.id;
});
test("can create role", async () => {
state.roleName = "Test Role";
const response = await apiPost(
`role`,
{
name: state.roleName,
communityId: state.communityId,
permissions: ["MEMBERS_READ", "CHANNELS_READ", "ROLES_READ"],
},
state.token1,
);
assert.equal(validate(response.id), true);
assert.equal(response.name, state.roleName);
assert.equal(response.communityId, state.communityId);
state.roleId = response.id;
});
test("can assign role to user", async () => {
const response = await apiGet(
`community/${state.communityId}/members`,
state.token2,
);
assert.equal(response.id, state.communityId);
assert.equal(response.name, state.communityName);
assert.equal(response.members.length === 2, true);
});
test("can get members", async () => {
const response = await apiGet(
`community/${state.communityId}/members`,
state.token2,
);
assert.equal(response.id, state.communityId);
assert.equal(response.name, state.communityName);
assert.equal(response.members.length === 2, true);
});
test("can get channels", async () => {
const response = await apiGet(
`community/${state.communityId}/channels`,
state.token2,
);
assert.equal(response.id, state.communityId);
assert.equal(response.name, state.communityName);
assert.equal(response.channels.length === 1, true);
});
test("can get roles", async () => {
const response = await apiGet(
`community/${state.communityId}/roles`,
state.token2,
);
assert.equal(response.id, state.communityId);
assert.equal(response.name, state.communityName);
assert.equal(response.roles.length === 1, true);
});

View file

@ -1,33 +0,0 @@
import assert from "node:assert";
import { test } from "node:test";
import { validate } from "uuid";
import { apiGet, apiPost, apiPatch, apiDelete } from "./api.js";
const state = {};
test("can create community", async () => {
state.communityName = "testCommunity";
state.username1 = "testuser1";
state.password1 = "8556";
state.email1 = "testuser1@test.test";
state.username2 = "testuser2";
state.password2 = "8556";
state.email2 = "testuser2@test.test";
const response1 = await apiPost(`auth/login`, {
username: state.username1,
password: state.password1,
});
state.sessionId1 = response1.id;
state.token1 = response1.token;
const response2 = await apiPost(`auth/login`, {
username: state.username2,
password: state.password2,
});
state.sessionId2 = response2.id;
state.token2 = response2.token;
});
// TO-DO: Create community test and code