Add messaging
This commit is contained in:
parent
23128f25e1
commit
5733975aa0
29 changed files with 986 additions and 8 deletions
|
|
@ -13,12 +13,16 @@ import type {
|
|||
IDeleteChannelParams,
|
||||
IDeleteChannelResponseError,
|
||||
IDeleteChannelResponseSuccess,
|
||||
IGetMessagesParams,
|
||||
IGetMessagesResponseError,
|
||||
IGetMessagesResponseSuccess,
|
||||
} from "./types.js";
|
||||
import {
|
||||
createChannelAuth,
|
||||
deleteChannelByIdAuth,
|
||||
getChannelByIdAuth,
|
||||
updateChannelByIdAuth,
|
||||
getChannelMessagesByIdAuth,
|
||||
} from "../../services/channel/channel.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
|
||||
|
|
@ -135,4 +139,42 @@ const deleteChannel = async (request: FastifyRequest, reply: FastifyReply) => {
|
|||
} as IDeleteChannelResponseSuccess;
|
||||
};
|
||||
|
||||
export { getChannel, postCreateChannel, patchChannel, deleteChannel };
|
||||
const getMessages = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetMessagesParams;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const messages = await getChannelMessagesByIdAuth(id, authHeader);
|
||||
if (!messages) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IGetMessagesResponseError;
|
||||
}
|
||||
if (messages === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IGetMessagesResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: id,
|
||||
messages: messages.map((message) => ({
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
})),
|
||||
} as IGetMessagesResponseSuccess;
|
||||
};
|
||||
|
||||
export {
|
||||
getChannel,
|
||||
postCreateChannel,
|
||||
patchChannel,
|
||||
deleteChannel,
|
||||
getMessages,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const channelRoutes = async (fastify: FastifyInstance) => {
|
|||
fastify.post(`/`, controller.postCreateChannel);
|
||||
fastify.patch(`/:id`, controller.patchChannel);
|
||||
fastify.delete(`/:id`, controller.deleteChannel);
|
||||
fastify.get(`/:id/messages`, controller.getMessages);
|
||||
};
|
||||
|
||||
export { channelRoutes };
|
||||
|
|
|
|||
|
|
@ -62,6 +62,28 @@ interface IDeleteChannelResponseSuccess {
|
|||
communityId: string;
|
||||
}
|
||||
|
||||
interface IGetMessagesParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IGetMessagesResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IGetMessagesResponseSuccess {
|
||||
id: string;
|
||||
messages: IGetMessagesResponseMessage[];
|
||||
}
|
||||
|
||||
interface IGetMessagesResponseMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
edited: boolean;
|
||||
ownerId: string;
|
||||
creationDate: number;
|
||||
}
|
||||
|
||||
export {
|
||||
type IChannel,
|
||||
type IGetChannelParams,
|
||||
|
|
@ -77,4 +99,8 @@ export {
|
|||
type IDeleteChannelParams,
|
||||
type IDeleteChannelResponseError,
|
||||
type IDeleteChannelResponseSuccess,
|
||||
type IGetMessagesParams,
|
||||
type IGetMessagesResponseError,
|
||||
type IGetMessagesResponseSuccess,
|
||||
type IGetMessagesResponseMessage,
|
||||
};
|
||||
|
|
|
|||
3
src/controllers/message/index.ts
Normal file
3
src/controllers/message/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./message.js";
|
||||
export * from "./routes.js";
|
||||
export * from "./types.js";
|
||||
145
src/controllers/message/message.ts
Normal file
145
src/controllers/message/message.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||
import type {
|
||||
IGetMessageParams,
|
||||
IGetMessageResponseError,
|
||||
IGetMessageResponseSuccess,
|
||||
IPostCreateMessageRequest,
|
||||
IPostCreateMessageResponseError,
|
||||
IPostCreateMessageResponseSuccess,
|
||||
IPatchMessageParams,
|
||||
IPatchMessageRequest,
|
||||
IPatchMessageResponseError,
|
||||
IPatchMessageResponseSuccess,
|
||||
IDeleteMessageParams,
|
||||
IDeleteMessageResponseError,
|
||||
IDeleteMessageResponseSuccess,
|
||||
} from "./types.js";
|
||||
import {
|
||||
createMessageAuth,
|
||||
deleteMessageByIdAuth,
|
||||
getMessageByIdAuth,
|
||||
updateMessageByIdAuth,
|
||||
} from "../../services/message/message.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
|
||||
const getMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IGetMessageParams;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await getMessageByIdAuth(id, authHeader);
|
||||
if (!message) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IGetMessageResponseError;
|
||||
}
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IGetMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
editHistory: message.editHistory,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
} as IGetMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const postCreateMessage = async (
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const createMessageRequest = request.body as IPostCreateMessageRequest;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await createMessageAuth(createMessageRequest, authHeader);
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IPostCreateMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
editHistory: message.editHistory,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
} as IPostCreateMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const patchMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IPatchMessageParams;
|
||||
const patchMessageRequest = request.body as IPatchMessageRequest;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await updateMessageByIdAuth(
|
||||
id,
|
||||
patchMessageRequest,
|
||||
authHeader,
|
||||
);
|
||||
if (!message) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IPatchMessageResponseError;
|
||||
}
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IPatchMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
editHistory: message.editHistory,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
} as IPatchMessageResponseSuccess;
|
||||
};
|
||||
|
||||
const deleteMessage = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { id } = request.params as IDeleteMessageParams;
|
||||
const authHeader = request.headers["authorization"];
|
||||
|
||||
const message = await deleteMessageByIdAuth(id, authHeader);
|
||||
if (!message) {
|
||||
reply.status(404);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.NOT_FOUND,
|
||||
} as IDeleteMessageResponseError;
|
||||
}
|
||||
if (message === API_ERROR.ACCESS_DENIED) {
|
||||
reply.status(403);
|
||||
return {
|
||||
id: id,
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IDeleteMessageResponseError;
|
||||
}
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
ownerId: message.ownerId,
|
||||
channelId: message.channelId,
|
||||
} as IDeleteMessageResponseSuccess;
|
||||
};
|
||||
|
||||
export { getMessage, postCreateMessage, patchMessage, deleteMessage };
|
||||
11
src/controllers/message/routes.ts
Normal file
11
src/controllers/message/routes.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { type FastifyInstance } from "fastify";
|
||||
import * as controller from "./message.js";
|
||||
|
||||
const messageRoutes = async (fastify: FastifyInstance) => {
|
||||
fastify.get(`/:id`, controller.getMessage);
|
||||
fastify.post(`/`, controller.postCreateMessage);
|
||||
fastify.patch(`/:id`, controller.patchMessage);
|
||||
fastify.delete(`/:id`, controller.deleteMessage);
|
||||
};
|
||||
|
||||
export { messageRoutes };
|
||||
81
src/controllers/message/types.ts
Normal file
81
src/controllers/message/types.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import type { API_ERROR } from "../errors.js";
|
||||
|
||||
interface IMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
editHistory: string[];
|
||||
edited: boolean;
|
||||
ownerId: string;
|
||||
channelId: string;
|
||||
creationDate: number;
|
||||
}
|
||||
|
||||
interface IGetMessageParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IGetMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IGetMessageResponseSuccess extends IMessage {}
|
||||
|
||||
interface IPostCreateMessageRequest {
|
||||
text: string;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
interface IPostCreateMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IPostCreateMessageResponseSuccess extends IMessage {}
|
||||
|
||||
interface IPatchMessageParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IPatchMessageRequest {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface IPatchMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IPatchMessageResponseSuccess extends IMessage {}
|
||||
|
||||
interface IDeleteMessageParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface IDeleteMessageResponseError {
|
||||
id: string;
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
interface IDeleteMessageResponseSuccess {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
export {
|
||||
type IMessage,
|
||||
type IGetMessageParams,
|
||||
type IGetMessageResponseError,
|
||||
type IGetMessageResponseSuccess,
|
||||
type IPostCreateMessageRequest,
|
||||
type IPostCreateMessageResponseError,
|
||||
type IPostCreateMessageResponseSuccess,
|
||||
type IPatchMessageParams,
|
||||
type IPatchMessageRequest,
|
||||
type IPatchMessageResponseError,
|
||||
type IPatchMessageResponseSuccess,
|
||||
type IDeleteMessageParams,
|
||||
type IDeleteMessageResponseError,
|
||||
type IDeleteMessageResponseSuccess,
|
||||
};
|
||||
3
src/controllers/websocket/index.ts
Normal file
3
src/controllers/websocket/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./websocket.js";
|
||||
export * from "./routes.js";
|
||||
export * from "./types.js";
|
||||
12
src/controllers/websocket/routes.ts
Normal file
12
src/controllers/websocket/routes.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { type FastifyInstance } from "fastify";
|
||||
import * as controller from "./websocket.js";
|
||||
|
||||
const websocketRoutes = async (fastify: FastifyInstance) => {
|
||||
fastify.get(
|
||||
`/`,
|
||||
{ websocket: true, preHandler: controller.handleWebSockets },
|
||||
controller.getWebSockets,
|
||||
);
|
||||
};
|
||||
|
||||
export { websocketRoutes };
|
||||
7
src/controllers/websocket/types.ts
Normal file
7
src/controllers/websocket/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { API_ERROR } from "../errors.js";
|
||||
|
||||
interface IGetWebSocketResponseError {
|
||||
error: API_ERROR;
|
||||
}
|
||||
|
||||
export { type IGetWebSocketResponseError };
|
||||
33
src/controllers/websocket/websocket.ts
Normal file
33
src/controllers/websocket/websocket.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { type FastifyReply, type FastifyRequest } from "fastify";
|
||||
import type { IGetWebSocketResponseError } from "./types.js";
|
||||
import { API_ERROR } from "../errors.js";
|
||||
import { getUserFromCookie } from "../../services/auth/helpers.js";
|
||||
import { handleNewWebSocket } from "../../services/websocket/websocket.js";
|
||||
import type { WebSocket } from "@fastify/websocket";
|
||||
|
||||
const handleWebSockets = async (
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const cookie = request.cookies["token"];
|
||||
const user = await getUserFromCookie(cookie);
|
||||
if (!user) {
|
||||
reply.status(403);
|
||||
return {
|
||||
error: API_ERROR.ACCESS_DENIED,
|
||||
} as IGetWebSocketResponseError;
|
||||
}
|
||||
};
|
||||
|
||||
const getWebSockets = async (socket: WebSocket, request: FastifyRequest) => {
|
||||
const cookie = request.cookies["token"];
|
||||
const user = await getUserFromCookie(cookie);
|
||||
if (!user) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
handleNewWebSocket(socket, user.id);
|
||||
};
|
||||
|
||||
export { handleWebSockets, getWebSockets };
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import cookie from "@fastify/cookie";
|
||||
import websocket from "@fastify/websocket";
|
||||
|
||||
import { config } from "./config.js";
|
||||
|
||||
|
|
@ -14,6 +15,8 @@ import { communityRoutes } from "./controllers/community/routes.js";
|
|||
import { channelRoutes } from "./controllers/channel/routes.js";
|
||||
import { roleRoutes } from "./controllers/role/routes.js";
|
||||
import { inviteRoutes } from "./controllers/invite/routes.js";
|
||||
import { messageRoutes } from "./controllers/message/routes.js";
|
||||
import { websocketRoutes } from "./controllers/websocket/routes.js";
|
||||
|
||||
const app = Fastify({
|
||||
logger: true,
|
||||
|
|
@ -26,6 +29,8 @@ app.register(cors, {
|
|||
|
||||
app.register(cookie, { secret: getCookieSecret() });
|
||||
|
||||
app.register(websocket);
|
||||
|
||||
app.register(testRoutes);
|
||||
app.register(authRoutes, { prefix: "/api/v1/auth" });
|
||||
app.register(userRoutes, { prefix: "/api/v1/user" });
|
||||
|
|
@ -34,6 +39,8 @@ app.register(communityRoutes, { prefix: "/api/v1/community" });
|
|||
app.register(channelRoutes, { prefix: "/api/v1/channel" });
|
||||
app.register(roleRoutes, { prefix: "/api/v1/role" });
|
||||
app.register(inviteRoutes, { prefix: "/api/v1/invite" });
|
||||
app.register(messageRoutes, { prefix: "/api/v1/message" });
|
||||
app.register(websocketRoutes, { prefix: "/ws" });
|
||||
|
||||
app.listen({ port: config.port }, (err, address) => {
|
||||
if (err) throw err;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,31 @@ const verifyPassword = async (
|
|||
return await argon2.verify(passwordHash, passwordToCheck);
|
||||
};
|
||||
|
||||
const getUserFromCookie = async (
|
||||
cookie: string | undefined,
|
||||
): Promise<User | null> => {
|
||||
if (!cookie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await getDB().session.findFirst({
|
||||
where: {
|
||||
cookie: cookie,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await getUserBySessionId(session.id);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const getUserBySessionId = async (sessionId: string): Promise<User | null> => {
|
||||
return await getDB().user.findFirst({
|
||||
where: {
|
||||
|
|
@ -116,6 +141,12 @@ const isUserOwnerOrAdmin = async (
|
|||
if (ownerCheck.role !== undefined) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
ownerCheck.message !== undefined &&
|
||||
ownerCheck.message?.ownerId !== user.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
@ -214,6 +245,7 @@ export {
|
|||
verifyToken,
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
getUserFromCookie,
|
||||
getUserBySessionId,
|
||||
getUserFromAuth,
|
||||
isUserOwnerOrAdmin,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@ enum PERMISSION {
|
|||
MEMBERS_READ = "MEMBERS_READ",
|
||||
MEMBERS_KICK = "MEMBERS_KICK",
|
||||
MEMBERS_BAN = "MEMBERS_BAN",
|
||||
INVITES_READ = "INVITES_READ",
|
||||
INVITES_CREATE = "INVITES_CREATE",
|
||||
INVITES_DELETE = "INVITES_DELETE",
|
||||
MESSAGES_READ = "MESSAGES_READ",
|
||||
MESSAGES_CREATE = "MESSAGES_CREATE",
|
||||
MESSAGES_DELETE = "MESSAGES_DELETE",
|
||||
}
|
||||
|
||||
export { PERMISSION };
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
Role,
|
||||
Session,
|
||||
User,
|
||||
Message,
|
||||
} from "../../generated/prisma/client.js";
|
||||
|
||||
interface AccessTokenPayload extends JwtPayload {
|
||||
|
|
@ -30,6 +31,7 @@ interface IOwnerCheck {
|
|||
invite?: Invite | null;
|
||||
channel?: Channel | null;
|
||||
role?: Role | null;
|
||||
message?: Message | null;
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { API_ERROR } from "../../controllers/errors.js";
|
||||
import type { Channel } from "../../generated/prisma/client.js";
|
||||
import type { Channel, Message } from "../../generated/prisma/client.js";
|
||||
import { getDB } from "../../store/store.js";
|
||||
import { getUserFromAuth, isUserAllowed } from "../auth/helpers.js";
|
||||
import { PERMISSION } from "../auth/permission.js";
|
||||
|
|
@ -136,6 +136,40 @@ const deleteChannelByIdAuth = async (
|
|||
return await deleteChannelById(id);
|
||||
};
|
||||
|
||||
const getChannelMessagesById = async (
|
||||
id: string,
|
||||
): Promise<Message[] | null> => {
|
||||
return await getDB().message.findMany({
|
||||
where: {
|
||||
channelId: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getChannelMessagesByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message[] | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const channel = await getChannelById(id);
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_READ],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await getChannelMessagesById(id);
|
||||
};
|
||||
|
||||
export {
|
||||
getChannelById,
|
||||
getChannelByIdAuth,
|
||||
|
|
@ -145,4 +179,6 @@ export {
|
|||
updateChannelByIdAuth,
|
||||
deleteChannelById,
|
||||
deleteChannelByIdAuth,
|
||||
getChannelMessagesById,
|
||||
getChannelMessagesByIdAuth,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ const getCommunityInvitesByIdAuth = async (
|
|||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.INVITES_CREATE],
|
||||
[PERMISSION.INVITES_READ],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
|
|
|
|||
2
src/services/message/index.ts
Normal file
2
src/services/message/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./message.js";
|
||||
export * from "./types.js";
|
||||
202
src/services/message/message.ts
Normal file
202
src/services/message/message.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { API_ERROR } from "../../controllers/errors.js";
|
||||
import type { Message } from "../../generated/prisma/client.js";
|
||||
import { getDB } from "../../store/store.js";
|
||||
import {
|
||||
getUserFromAuth,
|
||||
isUserAllowed,
|
||||
isUserInCommunity,
|
||||
isUserOwnerOrAdmin,
|
||||
} from "../auth/helpers.js";
|
||||
import { PERMISSION } from "../auth/permission.js";
|
||||
import { getChannelById } from "../channel/channel.js";
|
||||
import { getCommunityById } from "../community/community.js";
|
||||
import { SocketMessageTypes } from "../websocket/types.js";
|
||||
import { sendMessageToUsers } from "../websocket/websocket.js";
|
||||
import type { ICreateMessage, IUpdateMessage } from "./types.js";
|
||||
|
||||
const getMessageById = async (id: string): Promise<Message | null> => {
|
||||
return await getDB().message.findUnique({
|
||||
where: { id: id },
|
||||
});
|
||||
};
|
||||
|
||||
const getMessageByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_READ],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const createMessage = async (
|
||||
ownerId: string,
|
||||
communityId: string,
|
||||
create: ICreateMessage,
|
||||
): Promise<Message> => {
|
||||
const message = await getDB().message.create({
|
||||
data: {
|
||||
ownerId: ownerId,
|
||||
...create,
|
||||
},
|
||||
});
|
||||
|
||||
const usersInCommunity = await getDB().user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
communities: {
|
||||
some: {
|
||||
id: communityId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const userIds = usersInCommunity.map((user) => user.id);
|
||||
|
||||
sendMessageToUsers(userIds, {
|
||||
type: SocketMessageTypes.NEW_MESSAGE,
|
||||
payload: {
|
||||
channelId: message.channelId,
|
||||
message: {
|
||||
id: message.id,
|
||||
text: message.text,
|
||||
edited: message.edited,
|
||||
ownerId: message.ownerId,
|
||||
creationDate: message.creationDate.getTime(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const createMessageAuth = async (
|
||||
create: ICreateMessage,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const channel = await getChannelById(create.channelId);
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!authUser ||
|
||||
!community ||
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_CREATE],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await createMessage(authUser.id, community.id, create);
|
||||
};
|
||||
|
||||
const updateMessageById = async (
|
||||
id: string,
|
||||
update: IUpdateMessage,
|
||||
): Promise<Message | null> => {
|
||||
const message = await getMessageById(id);
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newEditHistory = [...message.editHistory, message.text];
|
||||
|
||||
return await getDB().message.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
...update,
|
||||
editHistory: newEditHistory,
|
||||
edited: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateMessageByIdAuth = async (
|
||||
id: string,
|
||||
update: IUpdateMessage,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserOwnerOrAdmin(authUser, {
|
||||
message: message,
|
||||
})) ||
|
||||
!(await isUserInCommunity(authUser, community))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await updateMessageById(id, update);
|
||||
};
|
||||
|
||||
const deleteMessageById = async (id: string): Promise<Message | null> => {
|
||||
return await getDB().message.delete({
|
||||
where: { id: id },
|
||||
});
|
||||
};
|
||||
|
||||
const deleteMessageByIdAuth = async (
|
||||
id: string,
|
||||
authHeader: string | undefined,
|
||||
): Promise<Message | null | API_ERROR.ACCESS_DENIED> => {
|
||||
const authUser = await getUserFromAuth(authHeader);
|
||||
const message = await getMessageById(id);
|
||||
const channel = await getChannelById(message?.channelId ?? "");
|
||||
const community = await getCommunityById(channel?.communityId ?? "");
|
||||
|
||||
if (
|
||||
!(await isUserAllowed(
|
||||
authUser,
|
||||
{
|
||||
community: community,
|
||||
},
|
||||
community,
|
||||
[PERMISSION.MESSAGES_DELETE],
|
||||
))
|
||||
) {
|
||||
return API_ERROR.ACCESS_DENIED;
|
||||
}
|
||||
|
||||
return await deleteMessageById(id);
|
||||
};
|
||||
|
||||
export {
|
||||
getMessageById,
|
||||
getMessageByIdAuth,
|
||||
createMessage,
|
||||
createMessageAuth,
|
||||
updateMessageById,
|
||||
updateMessageByIdAuth,
|
||||
deleteMessageById,
|
||||
deleteMessageByIdAuth,
|
||||
};
|
||||
10
src/services/message/types.ts
Normal file
10
src/services/message/types.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
interface ICreateMessage {
|
||||
text: string;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
interface IUpdateMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export { type ICreateMessage, type IUpdateMessage };
|
||||
2
src/services/websocket/index.ts
Normal file
2
src/services/websocket/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./websocket.js";
|
||||
export * from "./types.js";
|
||||
53
src/services/websocket/types.ts
Normal file
53
src/services/websocket/types.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { WebSocket } from "@fastify/websocket";
|
||||
import type { IGetMessagesResponseMessage } from "../../controllers/channel/types.js";
|
||||
|
||||
interface ISocketConnection {
|
||||
socket: WebSocket;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
enum SocketRequestTypes {
|
||||
PING = "PING",
|
||||
}
|
||||
|
||||
enum SocketMessageTypes {
|
||||
NEW_ANNOUNCEMENT = "NEW_ANNOUNCEMENT",
|
||||
NEW_MESSAGE = "NEW_MESSAGE",
|
||||
NEW_CHANNEL = "NEW_CHANNEL",
|
||||
}
|
||||
|
||||
type SocketRequest = {
|
||||
type: SocketRequestTypes.PING;
|
||||
};
|
||||
|
||||
type SocketMessage =
|
||||
| {
|
||||
type: SocketMessageTypes.NEW_ANNOUNCEMENT;
|
||||
payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SocketMessageTypes.NEW_MESSAGE;
|
||||
payload: {
|
||||
channelId: string;
|
||||
message: IGetMessagesResponseMessage;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: SocketMessageTypes.NEW_CHANNEL;
|
||||
payload: {
|
||||
id: string;
|
||||
communityId: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
type ISocketConnection,
|
||||
SocketRequestTypes,
|
||||
SocketMessageTypes,
|
||||
type SocketRequest,
|
||||
type SocketMessage,
|
||||
};
|
||||
75
src/services/websocket/websocket.ts
Normal file
75
src/services/websocket/websocket.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { CloseEvent, MessageEvent } from "ws";
|
||||
import type { WebSocket } from "@fastify/websocket";
|
||||
import type {
|
||||
ISocketConnection,
|
||||
SocketRequest,
|
||||
SocketMessage,
|
||||
} from "./types.js";
|
||||
|
||||
const userConnections = new Map<string, Set<ISocketConnection>>();
|
||||
|
||||
const handleNewWebSocket = (socket: WebSocket, userId: string) => {
|
||||
const connection = {
|
||||
socket: socket,
|
||||
userId: userId,
|
||||
};
|
||||
|
||||
if (!userConnections.has(userId)) {
|
||||
userConnections.set(userId, new Set());
|
||||
}
|
||||
userConnections.get(userId)?.add(connection);
|
||||
|
||||
onCloseWsHandler(connection);
|
||||
onMessageWsHandler(connection);
|
||||
};
|
||||
|
||||
const handleRequest = (data: string): SocketRequest | null => {
|
||||
const request = JSON.parse(data) as SocketRequest;
|
||||
|
||||
if (!request.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const onCloseWsHandler = (connection: ISocketConnection) => {
|
||||
connection.socket.onclose = (event: CloseEvent) => {
|
||||
const connections = userConnections.get(connection.userId);
|
||||
|
||||
connections?.delete(connection);
|
||||
if (connections?.size === 0) {
|
||||
userConnections.delete(connection.userId);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onMessageWsHandler = (connection: ISocketConnection) => {
|
||||
connection.socket.onmessage = (event: MessageEvent) => {
|
||||
const request = handleRequest(event.data.toString());
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const sendMessageToUser = (userId: string, message: SocketMessage) => {
|
||||
const connections = userConnections.get(userId);
|
||||
|
||||
connections?.forEach((connection) => {
|
||||
connection.socket.send(JSON.stringify(message));
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessageToUsers = (userIds: string[], message: SocketMessage) => {
|
||||
userIds?.forEach((userId) => {
|
||||
sendMessageToUser(userId, message);
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
userConnections,
|
||||
handleNewWebSocket,
|
||||
sendMessageToUser,
|
||||
sendMessageToUsers,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue