diff --git a/concord-server/src/controller/realtime.ts b/concord-server/src/controller/realtime.ts index 7cacb9d..5f9b961 100644 --- a/concord-server/src/controller/realtime.ts +++ b/concord-server/src/controller/realtime.ts @@ -1,127 +1,126 @@ import { Context } from "hono"; -import { sendMessageToChannel, removeMessageFromChannel } from "../services/realtime.js" +import { + sendMessageToChannel, + removeMessageFromChannel, +} from "../services/realtime.js"; import { success } from "zod"; +export async function postMessageToChannel(io: any, c: Context) { + try { + io = c.get("io"); -export async function postMessageToChannel( - io: any, - c: Context -) { - try { - io = c.get("io"); + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const message = await c.req.json(); - const instanceId = c.req.param("instanceId"); - const categoryId = c.req.param("categoryId"); - const channelId = c.req.param("channelId"); - const message = await c.req.json(); - - const result = await sendMessageToChannel( - instanceId, - categoryId, - channelId, - message, - "new_channel_message", - io - ) + const result = await sendMessageToChannel( + instanceId, + categoryId, + channelId, + message, + "new_channel_message", + io, + ); - if(result === "Event not implemented"){ - console.log("controller::realtime::postMessageToChannel - Failed to send message") - return c.json({ - success: false, - message: "Event not implemented or recognized", - status: 400 - }) - } - - if(result === "no acknowledgment"){ - console.log("controller::realtime::postMessageToChannel - No acknowledgment received from client") - return c.json({ - success: false, - message: "No acknowledgment received from client", - status: 500 - }) - } - - if(!result){ - throw new Error("failed to send message"); - } - - return c.json({ - success: true, - message: "Message sent successfully", - status: 200 - }) - - - } catch (err) { - const errMessage = err as Error; - console.log("controller::realtime::postMessageToChannel - ", errMessage); - return c.json({ - success: false, - message: errMessage.message, - status: 500 - }); + if (result === "Event not implemented") { + console.log( + "controller::realtime::postMessageToChannel - Failed to send message", + ); + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400, + }); } + + if (result === "no acknowledgment") { + console.log( + "controller::realtime::postMessageToChannel - No acknowledgment received from client", + ); + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500, + }); + } + + if (!result) { + throw new Error("failed to send message"); + } + + return c.json({ + success: true, + message: "Message sent successfully", + status: 200, + }); + } catch (err) { + const errMessage = err as Error; + console.log("controller::realtime::postMessageToChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500, + }); + } } -export async function deleteMessageFromChannel( - io: any, - c: Context -){ - try { +export async function deleteMessageFromChannel(io: any, c: Context) { + try { + io = c.get("io"); - io = c.get("io"); + const instanceId = c.req.param("instanceId"); + const categoryId = c.req.param("categoryId"); + const channelId = c.req.param("channelId"); + const messageId = c.req.param("messageId"); - const instanceId = c.req.param("instanceId"); - const categoryId = c.req.param("categoryId"); - const channelId = c.req.param("channelId"); - const messageId = c.req.param("messageId"); + const result = await removeMessageFromChannel( + instanceId, + categoryId, + channelId, + messageId, + "delete_channel_message", + io, + ); - const result = await removeMessageFromChannel( - instanceId, - categoryId, - channelId, - messageId, - "delete_channel_message", - io - ) - - if(result === "event not implemented"){ - console.log("controller::realtime::deleteMessageFromChannel - Event not implemented") - return c.json({ - success: false, - message: "Event not implemented or recognized", - status: 400 - }); - } - - if(result === "no acknowledgment"){ - console.log("controller::realtime::deleteMessageFromChannel - No acknowledgment received from client") - return c.json({ - success: false, - message: "No acknowledgment received from client", - status: 500 - }); - } - - if(!result){ - throw new Error("failed to delete message"); - } - - c.json({ - success: true, - message: "Message deleted successfully", - status: 200 - }) - - } catch (err) { - - const errMessage = err as Error; - console.log("services::realtime::deleteMessageFromChannel - ", errMessage); - return c.json({ - success: false, - message: errMessage.message, - status: 500 - }); + if (result === "event not implemented") { + console.log( + "controller::realtime::deleteMessageFromChannel - Event not implemented", + ); + return c.json({ + success: false, + message: "Event not implemented or recognized", + status: 400, + }); } + + if (result === "no acknowledgment") { + console.log( + "controller::realtime::deleteMessageFromChannel - No acknowledgment received from client", + ); + return c.json({ + success: false, + message: "No acknowledgment received from client", + status: 500, + }); + } + + if (!result) { + throw new Error("failed to delete message"); + } + + c.json({ + success: true, + message: "Message deleted successfully", + status: 200, + }); + } catch (err) { + const errMessage = err as Error; + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return c.json({ + success: false, + message: errMessage.message, + status: 500, + }); + } } diff --git a/concord-server/src/controller/userController.ts b/concord-server/src/controller/userController.ts index 2e5af93..0ba558d 100644 --- a/concord-server/src/controller/userController.ts +++ b/concord-server/src/controller/userController.ts @@ -1,4 +1,8 @@ -import { getAllUsersFrom, getUserInformation, createUser } from "../services/userService"; +import { + getAllUsersFrom, + getUserInformation, + createUser, +} from "../services/userService"; import { CreateUserInput } from "../validators/userValidator"; export async function fetchUserData(id: string) { diff --git a/concord-server/src/helper/hashing.ts b/concord-server/src/helper/hashing.ts index cd4cb4f..d987684 100644 --- a/concord-server/src/helper/hashing.ts +++ b/concord-server/src/helper/hashing.ts @@ -1,5 +1,5 @@ -import * as crypto from 'crypto'; +import * as crypto from "crypto"; -export default function shaHash(data:string, salt:string) : string { - return crypto.createHmac('sha256', salt).update(data).digest('hex'); +export default function shaHash(data: string, salt: string): string { + return crypto.createHmac("sha256", salt).update(data).digest("hex"); } diff --git a/concord-server/src/index.ts b/concord-server/src/index.ts index 92d2d99..1348a03 100644 --- a/concord-server/src/index.ts +++ b/concord-server/src/index.ts @@ -53,21 +53,19 @@ app.use( app.route("/api", routes); app.get( - '/openapi', + "/openapi", openAPIRouteHandler(app, { documentation: { info: { - title: 'Hono API', - version: '1.0.0', - description: 'Greeting API', + title: "Hono API", + version: "1.0.0", + description: "Greeting API", }, - servers: [ - { url: 'http://localhost:3000', description: 'Local Server' }, - ], + servers: [{ url: "http://localhost:3000", description: "Local Server" }], }, - }) -) + }), +); -app.get('/scalar', Scalar({ url: '/openapi' })) +app.get("/scalar", Scalar({ url: "/openapi" })); export default app; diff --git a/concord-server/src/routes/realtime.ts b/concord-server/src/routes/realtime.ts index 27fb941..933b839 100644 --- a/concord-server/src/routes/realtime.ts +++ b/concord-server/src/routes/realtime.ts @@ -1,28 +1,29 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { describeRoute, resolver } from "hono-openapi"; -import { postMessageToChannel, - deleteMessageFromChannel +import { + postMessageToChannel, + deleteMessageFromChannel, } from "../controller/realtime"; const app = new Hono(); app.post( - "message/", - zValidator({ - body: z.object({ - content: z.string().min(1).max(500) - }) + "message/", + zValidator({ + body: z.object({ + content: z.string().min(1).max(500), }), - async (c) => { - const { instanceId, categoryId, channelId } = c.req.params; - const { content } = c.req.body; + }), + async (c) => { + const { instanceId, categoryId, channelId } = c.req.params; + const { content } = c.req.body; - return postMessageToChannel(c.get("io"), { - instanceId, - categoryId, - channelId, - content - }); - } -); \ No newline at end of file + return postMessageToChannel(c.get("io"), { + instanceId, + categoryId, + channelId, + content, + }); + }, +); diff --git a/concord-server/src/routes/userRoutes.ts b/concord-server/src/routes/userRoutes.ts index 4fc91f3..fd3fbf4 100644 --- a/concord-server/src/routes/userRoutes.ts +++ b/concord-server/src/routes/userRoutes.ts @@ -1,29 +1,38 @@ import { Hono } from "hono"; -import { fetchAllUsers, fetchUserData, createNewUser } from "../controller/userController"; -import { createUserSchema, queryAllUsersByInstanceId, queryUserByIdSchema } from "../validators/userValidator"; +import { + fetchAllUsers, + fetchUserData, + createNewUser, +} from "../controller/userController"; +import { + createUserSchema, + queryAllUsersByInstanceId, + queryUserByIdSchema, +} from "../validators/userValidator"; import { zValidator } from "@hono/zod-validator"; import { describeRoute, resolver } from "hono-openapi"; const actions = new Hono(); -actions.get("user/:id", +actions.get( + "user/:id", describeRoute({ description: "Get user by id", responses: { 200: { description: "Success getting user", content: { - "application/json": { schema: resolver(queryUserByIdSchema) } - } + "application/json": { schema: resolver(queryUserByIdSchema) }, + }, }, 404: { description: "User id not found", content: { - "application/json": { schema: resolver(queryUserByIdSchema) } - } - } - } + "application/json": { schema: resolver(queryUserByIdSchema) }, + }, + }, + }, }), - zValidator('param', queryUserByIdSchema), + zValidator("param", queryUserByIdSchema), async (c) => { const id = c.req.param("id"); const userData = await fetchUserData(id); @@ -32,22 +41,23 @@ actions.get("user/:id", } else { return c.json({ error: "User not found" }, 404); } - } + }, ); -actions.get("user", +actions.get( + "user", describeRoute({ description: "Get all users by instance id", responses: { 200: { description: "Success getting all users in instance", content: { - "application/json": { schema: resolver(queryAllUsersByInstanceId) } - } - } - } + "application/json": { schema: resolver(queryAllUsersByInstanceId) }, + }, + }, + }, }), - zValidator('query', queryAllUsersByInstanceId), + zValidator("query", queryAllUsersByInstanceId), async (c) => { const instanceId = c.req.query("instanceId"); if (!instanceId) { @@ -60,7 +70,7 @@ actions.get("user", } else { return c.json({ error: "Error getting all users from instance" }, 500); } - } + }, ); actions.post( @@ -71,18 +81,18 @@ actions.post( 201: { description: "Success", content: { - 'application/json': { schema: resolver(createUserSchema) }, + "application/json": { schema: resolver(createUserSchema) }, }, }, 400: { description: "Bad request (user exists)", content: { - 'application/json': { schema: resolver(createUserSchema) } - } - } - } + "application/json": { schema: resolver(createUserSchema) }, + }, + }, + }, }), - zValidator('json', createUserSchema), + zValidator("json", createUserSchema), async (c) => { try { const data = await c.req.json(); @@ -94,7 +104,7 @@ actions.post( } catch (error) { return c.json({ error: "Error creating user" }, 500); } - } + }, ); export default actions; diff --git a/concord-server/src/services/message.ts b/concord-server/src/services/message.ts index 4e666ee..f90550e 100644 --- a/concord-server/src/services/message.ts +++ b/concord-server/src/services/message.ts @@ -3,75 +3,76 @@ import { MessagePing, PrismaClient, Role, - Reply - + Reply, } from "@prisma/client"; -import { CreateUserInput } from '../validators/userValidator'; +import { CreateUserInput } from "../validators/userValidator"; const prisma = new PrismaClient(); -class MessageService { - public async function sendMessageToChannel( - channelId: string, - userId: string, - content: string, - repliedMessageId: string | null - ): Promise<{ - id: string, - channelId: string, - userId: string, - text: string, - deleted: boolean, - replies: null | { - messageId: string, - repliesToId: string, - repliesToText: string - } - } | null> { - try { - - - const newMessage = await prisma.message.create({ - data: { - channelId: channelId, - userId: userId, - text: content, - deleted: false, - - } - }) - - let origMessage; - if(repliedMessageId){ - origMessage = await prisma.message.findUnique({ - where: { - id: repliedMessageId - } - }) - - if(!origMessage){ - throw new Error("could not find original message to reply to"); - } - - await prisma.reply.create({ - data: { - messageId: newMessage.id, - repliesToId: origMessage.id - } - }) - } - - return { - ...newMessage, - replies: repliedMessageId ? { - messageId: newMessage.id, - repliesToId: origMessage?.id, - repliesToText: origMessage?.text - } : null - } - - } catch (error) { - +export async function sendMessageToChannel( + channelId: string, + userId: string, + content: string, + repliedMessageId: string | null, +): Promise<{ + id: string; + channelId: string; + userId: string; + text: string; + deleted: boolean; + replies: null | { + messageId: string; + repliesToId: string; + repliesToText: string; + }; +} | null> { + try { + const newMessage = await prisma.message.create({ + data: { + channelId: channelId, + userId: userId, + text: content, + deleted: false, + }, + }); + if (!newMessage) { + return null; } -} \ No newline at end of file + + let origMessage; + if (repliedMessageId) { + origMessage = await prisma.message.findUnique({ + where: { + id: repliedMessageId, + }, + }); + + if (!origMessage) { + throw new Error("could not find original message to reply to"); + } + + await prisma.reply.create({ + data: { + messageId: newMessage.id, + repliesToId: origMessage.id, + }, + }); + } + + return { + ...newMessage, + channelId: newMessage.channelId!, + userId: newMessage.userId!, + replies: origMessage + ? { + messageId: newMessage.id, + repliesToId: origMessage?.id, + repliesToText: origMessage?.text, + } + : null, + }; + } catch (error) { + return null; + } +} diff --git a/concord-server/src/services/realtime.ts b/concord-server/src/services/realtime.ts index b99c5cd..434de72 100644 --- a/concord-server/src/services/realtime.ts +++ b/concord-server/src/services/realtime.ts @@ -1,88 +1,90 @@ import { readonly } from "zod"; const EVENTS = { - NEW_CHANNEL_MESSAGE: "new_channel_message", - DELETE_CHANNEL_MESSAGE: "delete_channel_message", -} - - + NEW_CHANNEL_MESSAGE: "new_channel_message", + DELETE_CHANNEL_MESSAGE: "delete_channel_message", +}; export async function sendMessageToChannel( - instanceId: string, - categoryId: string, - channelId: string, - message: any, - event: string, - io: any, + instanceId: string, + categoryId: string, + channelId: string, + message: any, + event: string, + io: any, ): Promise { - try { - - //TODO: implement middleware to replace this - if(EVENTS.NEW_CHANNEL_MESSAGE === event){ - throw new Error("Event not implemented"); - } - - //TODO: add prisma to save channel message to DB - - return new Promise((resolve) => { - io.to(instanceId).emit(event, message, (ack: any) => { - if (ack && ack.status === 'received') { - console.log(`Message ${ack.messageId} acknowledged by client.`); - resolve(true); - } else { - console.log('services::realtime::sendMessageToChannel No acknowledgment received from client.'); - resolve("no acknowledgment"); - } - }); - }); - } catch (err) { - const errMessage = err as Error; - if (errMessage.message === "Event not implemented") { - console.log(`services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`) - return "event not implemented" - } - console.log("services::realtime::sendMessageToChannel - ", errMessage); - return false; - + try { + //TODO: implement middleware to replace this + if (EVENTS.NEW_CHANNEL_MESSAGE === event) { + throw new Error("Event not implemented"); } + + //TODO: add prisma to save channel message to DB + + return new Promise((resolve) => { + io.to(instanceId).emit(event, message, (ack: any) => { + if (ack && ack.status === "received") { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log( + "services::realtime::sendMessageToChannel No acknowledgment received from client.", + ); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log( + `services::realtime::sendMessageToChannel - Event not implemented. Attempted event: ${event}`, + ); + return "event not implemented"; + } + console.log("services::realtime::sendMessageToChannel - ", errMessage); + return false; + } } export async function removeMessageFromChannel( - instanceId: string, - categoryId: string, - channelId: string, - messageId: string, - event: string, - io: any -): Promise{ - try { - - //TODO: implement middleware to replace this - if(EVENTS.DELETE_CHANNEL_MESSAGE === event){ - throw new Error("event not implemented"); - } - - //TODO: add prisma to flag a channel message as deleted - - return new Promise((resolve) => { - io.to(instanceId).emit(event, { messageId }, (ack: any) => { - if (ack && ack.status === 'received') { - console.log(`Message ${ack.messageId} acknowledged by client.`); - resolve(true); - } else { - console.log('services::realtime::deleteMessageFromChannel No acknowledgment received from client.'); - resolve("no acknowledgment"); - } - }); - }); - } catch (err) { - const errMessage = err as Error; - if (errMessage.message === "Event not implemented") { - console.log(`services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`) - return false; - } - console.log("services::realtime::deleteMessageFromChannel - ", errMessage); - return false; - + instanceId: string, + categoryId: string, + channelId: string, + messageId: string, + event: string, + io: any, +): Promise { + try { + //TODO: implement middleware to replace this + if (EVENTS.DELETE_CHANNEL_MESSAGE === event) { + throw new Error("event not implemented"); } -} \ No newline at end of file + + //TODO: add prisma to flag a channel message as deleted + + return new Promise((resolve) => { + io.to(instanceId).emit(event, { messageId }, (ack: any) => { + if (ack && ack.status === "received") { + console.log(`Message ${ack.messageId} acknowledged by client.`); + resolve(true); + } else { + console.log( + "services::realtime::deleteMessageFromChannel No acknowledgment received from client.", + ); + resolve("no acknowledgment"); + } + }); + }); + } catch (err) { + const errMessage = err as Error; + if (errMessage.message === "Event not implemented") { + console.log( + `services::realtime::deleteMessageFromChannel - Event not implemented. Attempted event: ${event}`, + ); + return false; + } + console.log("services::realtime::deleteMessageFromChannel - ", errMessage); + return false; + } +} diff --git a/concord-server/src/services/userService.ts b/concord-server/src/services/userService.ts index 13e96d9..eec7a47 100644 --- a/concord-server/src/services/userService.ts +++ b/concord-server/src/services/userService.ts @@ -5,31 +5,35 @@ import { Role, UserAuth, } from "@prisma/client"; -import { CreateUserInput } from '../validators/userValidator'; +import { CreateUserInput } from "../validators/userValidator"; import shaHash from "../helper/hashing"; const prisma = new PrismaClient(); export async function createUser(data: CreateUserInput): Promise<{ - username: string, - nickname: string | null, - bio: string | null, - picture: string | null, - banner: string | null, - status: string, - admin: boolean + username: string; + nickname: string | null; + bio: string | null; + picture: string | null; + banner: string | null; + status: string; + admin: boolean; } | null> { const requestingUser = await getUserInformation(data.requestingUserId); - const requestingUserCredentials = await getUserCredentials(data.requestingUserId) - if (!requestingUser - || !requestingUserCredentials - || !requestingUser.admin - || requestingUserCredentials.token == null - || data.requestingUserToken != requestingUserCredentials.token) { + const requestingUserCredentials = await getUserCredentials( + data.requestingUserId, + ); + if ( + !requestingUser || + !requestingUserCredentials || + !requestingUser.admin || + requestingUserCredentials.token == null || + data.requestingUserToken != requestingUserCredentials.token + ) { return null; } - if (await prisma.user.count({ where: { username: data.username }}) >= 1) { + if ((await prisma.user.count({ where: { username: data.username } })) >= 1) { return null; } @@ -45,13 +49,15 @@ export async function createUser(data: CreateUserInput): Promise<{ }, }); - if (!(await prisma.userAuth.create({ - data: { - userId: userData.id, - password: shaHash(data.passwordhash, userData.id), - token: null, - } - }))) { + if ( + !(await prisma.userAuth.create({ + data: { + userId: userData.id, + password: shaHash(data.passwordhash, userData.id), + token: null, + }, + })) + ) { return null; } @@ -59,52 +65,52 @@ export async function createUser(data: CreateUserInput): Promise<{ } export async function getUserCredentials(userId: string): Promise<{ - userId: string, - password: string, - token: string | null - } | null> { - try { - if (!userId) { - throw new Error("missing userId"); - } + userId: string; + password: string; + token: string | null; +} | null> { + try { + if (!userId) { + throw new Error("missing userId"); + } - const userAuth = await prisma.userAuth.findUnique({ - where: { - userId: userId, - }, - }); + const userAuth = await prisma.userAuth.findUnique({ + where: { + userId: userId, + }, + }); - if (!userAuth) { - throw new Error("could not find user credentials"); - } + if (!userAuth) { + throw new Error("could not find user credentials"); + } - return { - userId: userAuth.userId, - password: userAuth.password, - token: userAuth.token, - }; - } catch (err) { - const errMessage = err as Error; + return { + userId: userAuth.userId, + password: userAuth.password, + token: userAuth.token, + }; + } catch (err) { + const errMessage = err as Error; - if (errMessage.message === "missing userId") { - console.log("services::actions::getUserCredentials - missing userId"); - return null; - } - - if (errMessage.message === "could not find user credentials") { - console.log( - "services::actions::getUserCredentials - unable to find user credentials", - ); - return null; - } + if (errMessage.message === "missing userId") { + console.log("services::actions::getUserCredentials - missing userId"); + return null; + } + if (errMessage.message === "could not find user credentials") { console.log( - "services::actions::getUserCredentials - unknown error", - errMessage, + "services::actions::getUserCredentials - unable to find user credentials", ); return null; } + + console.log( + "services::actions::getUserCredentials - unknown error", + errMessage, + ); + return null; } +} export async function getUserInformation(userId: string): Promise<{ id: string; @@ -196,9 +202,9 @@ export async function getAllUsersFrom(instanceId: string): Promise< try { const instances = await prisma.instance.count({ where: { - id: instanceId - } - }) + id: instanceId, + }, + }); if (instances < 1) { throw new Error("could not find given instance id"); } @@ -218,8 +224,8 @@ export async function getAllUsersFrom(instanceId: string): Promise< const admins = await prisma.user.findMany({ where: { - admin: true - } + admin: true, + }, }); if (!admins) { throw new Error("could not get all admins"); @@ -249,13 +255,13 @@ export async function getAllUsersFrom(instanceId: string): Promise< ).includes(u.status as any) ? (u.status as "online" | "offline" | "dnd" | "idle" | "invis") : "offline", - role: adminRoles.map(r => ({ + role: adminRoles.map((r) => ({ userId: r.userId, instanceId: r.instanceId, })), - } - }) - ) + }; + }), + ); const userData = await Promise.all( users.map(async (u) => { diff --git a/concord-server/src/validators/userValidator.ts b/concord-server/src/validators/userValidator.ts index 463957a..9b9bfb6 100644 --- a/concord-server/src/validators/userValidator.ts +++ b/concord-server/src/validators/userValidator.ts @@ -1,12 +1,12 @@ -import { z } from 'zod' +import { z } from "zod"; export const queryUserByIdSchema = z.object({ - id: z.uuidv7() -}) + id: z.uuidv7(), +}); export const queryAllUsersByInstanceId = z.object({ - instanceId: z.uuidv7() -}) + instanceId: z.uuidv7(), +}); export const createUserSchema = z.object({ username: z.string().min(3).max(30), @@ -14,13 +14,17 @@ export const createUserSchema = z.object({ bio: z.string().max(500).optional(), picture: z.url().optional(), banner: z.url().optional(), - status: z.enum(['online', 'offline', 'dnd', 'idle', 'invis']).default('online'), + status: z + .enum(["online", "offline", "dnd", "idle", "invis"]) + .default("online"), admin: z.boolean().default(false), requestingUserId: z.uuidv7(), requestingUserToken: z.uuidv4(), passwordhash: z.string(), -}) +}); -export type QueryUserByIdInput = z.infer -export type QueryAllUsersByInstanceIdInput = z.infer -export type CreateUserInput = z.infer \ No newline at end of file +export type QueryUserByIdInput = z.infer; +export type QueryAllUsersByInstanceIdInput = z.infer< + typeof queryAllUsersByInstanceId +>; +export type CreateUserInput = z.infer;