Version 0.7.0

This commit is contained in:
Aslan 2026-01-20 14:08:51 -05:00
parent 603d969972
commit 8d3b0fa7d3
44 changed files with 611 additions and 41 deletions

View file

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

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Attachment" ADD COLUMN "iv" TEXT;

View file

@ -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;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Role" ADD COLUMN "showInMembers" BOOLEAN;

View file

@ -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";

View file

@ -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

View file

@ -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 };

View file

@ -0,0 +1,3 @@
export * from "./announcement.js";
export * from "./routes.js";
export * from "./types.js";

View file

@ -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 };

View file

@ -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 };

View file

@ -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;

View file

@ -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 {

View file

@ -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,
};

View file

@ -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);
};

View file

@ -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,

View file

@ -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,

View file

@ -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),

View file

@ -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;

View file

@ -45,6 +45,7 @@ interface IPatchMessageParams {
}
interface IPatchMessageRequest {
iv: string;
text: string;
}

View file

@ -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(),

View file

@ -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,
};

View file

@ -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 };

View file

@ -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,
};

View file

@ -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 };

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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" });

View file

@ -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<void | API_ERROR.ACCESS_DENIED> => {
const authUser = await getUserFromAuth(authHeader);
if (!authUser?.admin) {
//return API_ERROR.ACCESS_DENIED;
}
return await createAnnouncement(create);
};
export { createAnnouncement, createAnnouncementAuth };

View file

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

View file

@ -0,0 +1,6 @@
interface ICreateAnnouncement {
title: string;
text: string;
}
export { type ICreateAnnouncement };

View file

@ -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<Session | null> => {
userId: user.id,
name: sessionName,
userAgent: login.userAgent,
storageSecret: `${getRandomBytesHex(32)};${getRandomBytesHex(12)}`,
storageSecret: `${getRandomBytesBase64(32)};${getRandomBytesBase64(12)}`,
},
});
};

View file

@ -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,

View file

@ -177,6 +177,7 @@ const getChannelMessagesById = async (
id: string,
): Promise<FullMessage[] | null> => {
return await getDB().message.findMany({
take: 50,
include: {
reactions: {
select: {

View file

@ -5,6 +5,9 @@ interface ICreateChannel {
interface IUpdateChannel {
name?: string;
description?: string;
category?: string;
order?: number;
}
export { type ICreateChannel, type IUpdateChannel };

View file

@ -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<Community | null> => {
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<ICommunityRole[]> => {
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<ICommunityInvite[]> => {
const getCommunityInvitesById = async (id: string): Promise<Invite[]> => {
return await getDB().invite.findMany({
where: {
communityId: id,
},
select: {
id: true,
},
});
};
const getCommunityInvitesByIdAuth = async (
id: string,
authHeader: string | undefined,
): Promise<ICommunityInvite[] | API_ERROR.ACCESS_DENIED> => {
): Promise<Invite[] | API_ERROR.ACCESS_DENIED> => {
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<void | API_ERROR.ACCESS_DENIED> => {
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<void | API_ERROR.ACCESS_DENIED> => {
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,

View file

@ -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,
};

View file

@ -7,6 +7,7 @@ interface AttachmentWithChunks extends Attachment {
}
interface ICreateAttachment {
iv: string;
filename: string;
mimetype: string;
size: number;

View file

@ -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;
}

View file

@ -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);

View file

@ -18,6 +18,7 @@ interface ICreateMessage {
}
interface IUpdateMessage {
iv: string;
text: string;
}

View file

@ -8,6 +8,10 @@ interface ICreateRole {
interface IUpdateRole {
name?: string;
description?: string;
color?: string;
order?: number;
showInMembers?: boolean;
permissions?: PERMISSION[];
}

View file

@ -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<string, Set<string>>();
@ -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<Role[] | null> => {
return await getDB().role.findMany({
where: {
communityId: communityId,
users: {
some: {
id: id,
},
},
},
});
};
const getUserCommunityRolesByIdAuth = async (
id: string,
communityId: string,
authHeader: string | undefined,
): Promise<Role[] | null | API_ERROR.ACCESS_DENIED> => {
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<string[]> => {
@ -318,6 +357,8 @@ export {
getUserSessionsByIdAuth,
getUserCommunitiesById,
getUserCommunitiesByIdAuth,
getUserCommunityRolesById,
getUserCommunityRolesByIdAuth,
getUserIdsInCommunity,
getUserIdsInCommunityReadMessagesPermission,
getUserIdsWithRole,

View file

@ -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: {

View file

@ -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,
};