diff --git a/src/core/application/controllers/assistant/assistantController.ts b/src/core/application/controllers/assistant/assistantController.ts index b0a6e31..addd2db 100644 --- a/src/core/application/controllers/assistant/assistantController.ts +++ b/src/core/application/controllers/assistant/assistantController.ts @@ -1,14 +1,13 @@ import { Elysia, t } from "elysia"; import { ulid } from "ulid"; -import { getTokenPermissions, parseToken } from "../../services/tokenService"; -import { UNAUTHORIZED_NO_PERMISSION_CREATE_ASSISTANT } from "./returnValues"; -import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues"; +import { parseToken } from "../../services/tokenService"; import { assignRole, createUser, } from "@/core/application/services/userService"; import { createAssistant } from "@/core/application/services/assistantService"; +import { AuthMiddleware } from "../../middlewares/authorizationMiddleware"; type AssistantDecorator = { request: { @@ -23,21 +22,9 @@ export const assistants = new Elysia<"/assistant", AssistantDecorator>(); assistants.post( "/assistant", - async ({ bearer, set, body }) => { - if (!bearer) { - set.status = 401; - return UNAUTHORIZED_MISSING_TOKEN; - } - const permissions = await getTokenPermissions(bearer!); + async ({ bearer, body }) => { const decodedToken = await parseToken(bearer!); - if ( - !permissions?.some((p) => p.key === "create_assistant" || p.key === "*") - ) { - set.status = 403; - return UNAUTHORIZED_NO_PERMISSION_CREATE_ASSISTANT; - } - if (decodedToken) { const { userId } = decodedToken; const assistantId = ulid(); @@ -69,5 +56,6 @@ assistants.post( body: t.Object({ name: t.String(), }), - } + beforeHandle: AuthMiddleware(["create_assistant", "*"]), + }, ); diff --git a/src/core/application/controllers/thread/threadController.ts b/src/core/application/controllers/thread/threadController.ts index 7a52f95..a9a4dd2 100644 --- a/src/core/application/controllers/thread/threadController.ts +++ b/src/core/application/controllers/thread/threadController.ts @@ -10,14 +10,12 @@ import { } from "@/core/application/services/threadService"; import { THREAD_DELETED_SUCCESSFULLY, - UNAUTHORIZED_NO_PERMISSION_CREATE, UNAUTHORIZED_NO_PERMISSION_DELETE, - UNAUTHORIZED_NO_PERMISSION_READ, UNAUTHORIZED_USER_NOT_OWNER, UNAUTHORIZED_USER_NOT_PARTICIPANT, } from "./returnValues"; -import { UNAUTHORIZED_MISSING_TOKEN } from "@/core/application/ports/returnValues"; import { createMessage } from "../../services/messageService"; +import { AuthMiddleware } from "../../middlewares/authorizationMiddleware"; type ThreadDecorator = { request: { @@ -30,50 +28,42 @@ type ThreadDecorator = { export const threads = new Elysia<"/thread", ThreadDecorator>(); -threads.post("/thread", async ({ bearer, set }) => { - if (!bearer) { - set.status = 401; - return UNAUTHORIZED_MISSING_TOKEN; - } - const permissions = await getTokenPermissions(bearer!); - const decodedToken = await parseToken(bearer!); - - if (!permissions?.some((p) => p.key === "create_thread" || p.key === "*")) { - set.status = 403; - return UNAUTHORIZED_NO_PERMISSION_CREATE; - } - - if (decodedToken) { - const { userId } = decodedToken; - // // Create a new thread - const threadId = await createThread({ - id: ulid(), // Generate a unique ID for the thread - createdBy: userId, - participants: [], // Initialize the participants list - messageIds: [], // Initialize the message IDs list - }); +threads.post( + "/thread", + async ({ bearer }) => { + const decodedToken = await parseToken(bearer!); - // If the thread was created successfully, return it in the response - if (threadId) { - return { - id: threadId, - }; + if (decodedToken) { + const { userId } = decodedToken; + // // Create a new thread + const threadId = await createThread({ + id: ulid(), // Generate a unique ID for the thread + createdBy: userId, + participants: [], // Initialize the participants list + messageIds: [], // Initialize the message IDs list + }); + + // If the thread was created successfully, return it in the response + if (threadId) { + return { + id: threadId, + }; + } } - } -}); - -threads.delete("/thread/:id", async ({ params, bearer, set }) => { - const permissions = await getTokenPermissions(bearer!); - const decodedToken = await parseToken(bearer!); + }, + { + beforeHandle: AuthMiddleware(["create_thread", "*"]), + }, +); - if (decodedToken) { - const { userId } = decodedToken; - const threadId = params.id; +threads.delete( + "/thread/:id", + async ({ params, bearer, set }) => { + const decodedToken = await parseToken(bearer!); - // Check if the user has the permission to delete their own thread or * permission - if ( - permissions?.some((p) => p.key === "delete_own_thread" || p.key === "*") - ) { + if (decodedToken) { + const { userId, permissions } = decodedToken; + const threadId = params.id; // If the user has * permission, delete the thread without checking ownership if (permissions.some((p) => p.key === "*")) { await deleteThread(threadId, userId); @@ -87,89 +77,73 @@ threads.delete("/thread/:id", async ({ params, bearer, set }) => { set.status = 403; return UNAUTHORIZED_USER_NOT_OWNER; } - } else { - set.status = 403; - return UNAUTHORIZED_NO_PERMISSION_DELETE; } - } - set.status = 401; - return { message: "Unauthorized: Invalid or missing token", code: 401 }; -}); + }, + { + beforeHandle: AuthMiddleware(["delete_own_thread", "*"]), + }, +); -threads.get("/thread/:id", async ({ params, bearer, set }) => { - const permissions = await getTokenPermissions(bearer!); - const decodedToken = await parseToken(bearer!); +threads.get( + "/thread/:id", + async ({ params, bearer, set }) => { + const decodedToken = await parseToken(bearer!); - if (decodedToken) { - const { userId } = decodedToken; - const threadId = params.id; + if (decodedToken) { + const { userId } = decodedToken; + const threadId = params.id; + const isParticipant = await userOwnsOrParticipatesInThread( + threadId, + userId, + ); + const isSuperUser = decodedToken.permissions.some((p) => p.key === "*"); - // Check if the user has the permission to see their own threads or * permission - if ( - permissions?.some((p) => p.key === "view_own_threads" || p.key === "*") - ) { // If the user has * permission or is a participant in the thread, get the thread - if ( - permissions.some((p) => p.key === "*") || - (await userOwnsOrParticipatesInThread(threadId, userId)) - ) { + if (isParticipant || isSuperUser) { const thread = await getThread(threadId); return thread; } else { set.status = 403; return UNAUTHORIZED_USER_NOT_PARTICIPANT; } - } else { - set.status = 403; - return UNAUTHORIZED_NO_PERMISSION_READ; } - } - - set.status = 401; - return UNAUTHORIZED_MISSING_TOKEN; -}); + }, + { + beforeHandle: AuthMiddleware(["view_own_threads", "*"]), + }, +); threads.post( "/thread/:id/message", async ({ params, bearer, set, body }) => { - const permissions = await getTokenPermissions(bearer!); const decodedToken = await parseToken(bearer!); if (decodedToken) { - const { userId } = decodedToken; + const { userId, permissions } = decodedToken; const threadId = params.id; + const isSuperUser = permissions.some((p) => p.key === "*"); + const isParticipant = await userOwnsOrParticipatesInThread( + threadId, + userId, + ); // Check if the user has the permission to add a message - if ( - permissions?.some( - (p) => p.key === "create_message_in_own_thread" || p.key === "*" - ) - ) { - // if the user has * they can send a message anywhere, if not they need to be in conversation - if ( - permissions.some((p) => p.key === "*") || - (await userOwnsOrParticipatesInThread(threadId, userId)) - ) { - const { message } = body; - const res = await createMessage(userId, threadId, message); - set.status = 200; - return res; - } else { - set.status = 403; - return UNAUTHORIZED_USER_NOT_PARTICIPANT; - } + // if the user has * they can send a message anywhere, if not they need to be in conversation + if (isSuperUser || isParticipant) { + const { message } = body; + const res = await createMessage(userId, threadId, message); + set.status = 200; + return res; } else { set.status = 403; - return UNAUTHORIZED_NO_PERMISSION_READ; + return UNAUTHORIZED_USER_NOT_PARTICIPANT; } } - - set.status = 401; - return UNAUTHORIZED_MISSING_TOKEN; }, { body: t.Object({ message: t.String(), }), - } + beforeHandle: AuthMiddleware(["create_message_in_own_thread", "*"]), + }, ); diff --git a/src/core/application/controllers/thread/threadControllet.test.ts b/src/core/application/controllers/thread/threadControllet.test.ts index 431a243..4135119 100644 --- a/src/core/application/controllers/thread/threadControllet.test.ts +++ b/src/core/application/controllers/thread/threadControllet.test.ts @@ -11,6 +11,7 @@ describe.only("threadController", async () => { const request = new Request("http://localhost:8080/thread", { headers: { authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, method: "POST", }); diff --git a/src/core/application/controllers/user/userController.test.ts b/src/core/application/controllers/user/userController.test.ts index 7ac008e..521f89d 100644 --- a/src/core/application/controllers/user/userController.test.ts +++ b/src/core/application/controllers/user/userController.test.ts @@ -1,9 +1,11 @@ -import { createHumanUserForTesting, createSuperAdminForTesting } from "@/__tests__/utils"; +import { + createHumanUserForTesting, + createSuperAdminForTesting, +} from "@/__tests__/utils"; import { app } from "@/index"; import { test, expect, describe, beforeAll } from "bun:test"; import { getUser } from "../../services/userService"; import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues"; -import { UNAUTHORIZED_NO_PERMISSION_CREATE } from "./returnValues"; import { getThread } from "../../services/threadService"; import { parseToken } from "../../services/tokenService"; import { Thread } from "@/core/domain/thread"; @@ -21,13 +23,13 @@ describe.only("userController", async () => { const request = new Request("http://localhost:8080/users", { headers: { authorization: `Bearer ${superAdminToken}`, - "Content-Type": "application/json" + "Content-Type": "application/json", }, method: "POST", body: JSON.stringify({ name: "Mr. Sprout", - email: "wagmi@sprout.com" - }) + email: "wagmi@sprout.com", + }), }); const response = await app.handle(request); @@ -56,30 +58,34 @@ describe.only("userController", async () => { method: "POST", body: JSON.stringify({ name: "Mr. Sprout", - email: "wagmi@sprout.com" - }) + email: "wagmi@sprout.com", + }), }); - const response: any = await app.handle(request).then((response) => response.json()) + const response: any = await app + .handle(request) + .then((response) => response.json()); expect(response.message).toBe(UNAUTHORIZED_MISSING_TOKEN.message); }); - test("prevents from creating a user if the user does not have CREATE_USER permission", async () => { + test("prevents from creating a user if the user does not have create_user permission", async () => { const request = new Request("http://localhost:8080/users", { headers: { authorization: `Bearer ${humanUserToken}`, - "Content-Type": "application/json" + "Content-Type": "application/json", }, method: "POST", body: JSON.stringify({ name: "Jr. Sprout", - email: "ishouldnotexist@sprout.com" - }) + email: "ishouldnotexist@sprout.com", + }), }); const response: any = await app.handle(request); const responseJson = await response.json(); - expect(responseJson.message).toBe(UNAUTHORIZED_NO_PERMISSION_CREATE.message); + expect(responseJson.message).toBe( + "You do not have permission to perform create_user action", + ); }); test("allows a human user to create a thread", async () => { @@ -102,32 +108,40 @@ describe.only("userController", async () => { expect(thread).not.toBeNull(); expect(thread?.id).toEqual(id); - }) + }); test("allows a human user to add messages to a thread they created", async () => { // Creating a thread for the test - const createThreadResponse = await app.handle(new Request("http://localhost:8080/thread", { - headers: { - authorization: `Bearer ${humanUserToken}`, - "Content-type": "application/json", - }, - method: "POST", - })); - const thread: Thread = await createThreadResponse.json() as Thread; + const createThreadResponse = await app.handle( + new Request("http://localhost:8080/thread", { + headers: { + authorization: `Bearer ${humanUserToken}`, + "Content-type": "application/json", + }, + method: "POST", + }), + ); + const thread: Thread = (await createThreadResponse.json()) as Thread; expect(thread).toHaveProperty("id"); // Adding a message to the thread - const response = await app.handle(new Request(`http://localhost:8080/thread/${thread.id}/message`, { - headers: { - authorization: `Bearer ${humanUserToken}`, - "Content-type": "application/json", - }, - method: "POST", - body: JSON.stringify({ message: "Human user from userController test!" }) - })); - const messageResponse = await response.json() as any; + const response = await app.handle( + new Request(`http://localhost:8080/thread/${thread.id}/message`, { + headers: { + authorization: `Bearer ${humanUserToken}`, + "Content-type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + message: "Human user from userController test!", + }), + }), + ); + const messageResponse = (await response.json()) as any; expect(messageResponse).toHaveProperty("content"); - expect(messageResponse.content).toBe("Human user from userController test!"); + expect(messageResponse.content).toBe( + "Human user from userController test!", + ); }); -}) +}); diff --git a/src/core/application/controllers/user/userController.ts b/src/core/application/controllers/user/userController.ts index b4e33e7..53b4ddf 100644 --- a/src/core/application/controllers/user/userController.ts +++ b/src/core/application/controllers/user/userController.ts @@ -1,9 +1,11 @@ import Elysia, { t } from "elysia"; -import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues"; -import { createToken, getTokenPermissions } from "@/core/application/services/tokenService"; -import { UNAUTHORIZED_INVALID_TOKEN } from "../thread/returnValues"; -import { UNAUTHORIZED_NO_PERMISSION_CREATE, UNSUCCESSFUL_USER_CREATION, USER_CREATED_SUCCESSFULLY } from "./returnValues"; +import { createToken } from "@/core/application/services/tokenService"; +import { + UNSUCCESSFUL_USER_CREATION, + USER_CREATED_SUCCESSFULLY, +} from "./returnValues"; import { createHumanUser } from "../../services/userService"; +import { AuthMiddleware } from "../../middlewares/authorizationMiddleware"; type UserDecorator = { request: { @@ -21,46 +23,29 @@ export const users = new Elysia<"/users", UserDecorator>(); * @param {string | undefined} bearer - The bearer token. * @returns {Promise} - Returns an error if the token is missing, invalid or does not have CREATE_USER. */ -users.guard({ - beforeHandle: [async ({ bearer, set }) => { - if (!bearer) { - set.status = UNAUTHORIZED_MISSING_TOKEN.code; - return UNAUTHORIZED_MISSING_TOKEN; +users.post( + "/users", + async ({ set, body }) => { + // Human user uses the User role + const humanUserId = await createHumanUser(body); + if (humanUserId) { + const humanUserToken = await createToken(humanUserId); + + set.status = USER_CREATED_SUCCESSFULLY.code; + return { + ...USER_CREATED_SUCCESSFULLY, + token: humanUserToken, + }; } - // Check that the token is valid and has the required permission. - const permissions = await getTokenPermissions(bearer); - if (!permissions) { - set.status = UNAUTHORIZED_INVALID_TOKEN.code; - return UNAUTHORIZED_INVALID_TOKEN; - } - - if (!permissions.some(p => ["create_user", "*"].includes(p.key))) { - set.status = UNAUTHORIZED_NO_PERMISSION_CREATE.code; - return UNAUTHORIZED_NO_PERMISSION_CREATE; - } - }], -}) - - -users.post("/users", async ({ set, body }) => { - // Human user uses the User role - const humanUserId = await createHumanUser(body); - if (humanUserId) { - const humanUserToken = await createToken(humanUserId); - - set.status = USER_CREATED_SUCCESSFULLY.code; - return { - ...USER_CREATED_SUCCESSFULLY, - token: humanUserToken, - }; - } - - set.status = UNSUCCESSFUL_USER_CREATION.code; - return UNSUCCESSFUL_USER_CREATION; -}, { - body: t.Object({ - email: t.String(), - name: t.String(), - }), -}); + set.status = UNSUCCESSFUL_USER_CREATION.code; + return UNSUCCESSFUL_USER_CREATION; + }, + { + body: t.Object({ + email: t.String(), + name: t.String(), + }), + beforeHandle: AuthMiddleware(["create_user", "*"]), + }, +); diff --git a/src/core/application/middlewares/authorizationMiddleware.ts b/src/core/application/middlewares/authorizationMiddleware.ts new file mode 100644 index 0000000..e360735 --- /dev/null +++ b/src/core/application/middlewares/authorizationMiddleware.ts @@ -0,0 +1,54 @@ +import { Permission } from "@/core/domain/permissions"; +import { Context } from "elysia"; +import { UNAUTHORIZED_MISSING_TOKEN } from "../ports/returnValues"; +import { getTokenPermissions } from "../services/tokenService"; +import { UNAUTHORIZED_INVALID_TOKEN } from "./returnValues"; + +// Only need set and bearer from the context. +type AuthContext = Pick & { + bearer: string | undefined; +}; + +/** + * Middleware to check if the user has the required permissions. + * Middleware will ignore the required permissions if the user has the wildcard permission. + * @param {Permission[]} requiredPermissions - The required permissions. + * @returns {Promise} - Returns an error if the user does not have the required permissions. + */ +export function AuthMiddleware(requiredPermissions: Permission[]) { + return async (context: AuthContext) => { + const { bearer, set } = context; + if (!bearer) { + set.status = 401; + return UNAUTHORIZED_MISSING_TOKEN; + } + + // Check that the token is valid and has the required permission. + const tokenPermissions = await getTokenPermissions(bearer); + if (!tokenPermissions) { + set.status = UNAUTHORIZED_INVALID_TOKEN.code; + return UNAUTHORIZED_INVALID_TOKEN; + } + + const userPermissionKeys = tokenPermissions.map( + (permission) => permission.key, + ); + const hasWildcardPermission = userPermissionKeys.includes("*"); + + const invalidPermission = requiredPermissions.find((permission) => { + // If the user has wildcard, or required permission is '*', it's never invalid. + if (hasWildcardPermission || permission === "*") return false; + + // If the required permission is not included in the user's permissions, it's invalid. + return !userPermissionKeys.includes(permission); + }); + + if (invalidPermission) { + set.status = 403; + return { + code: 403, + message: `You do not have permission to perform ${invalidPermission} action`, + }; + } + }; +} diff --git a/src/core/application/middlewares/returnValues.ts b/src/core/application/middlewares/returnValues.ts new file mode 100644 index 0000000..04b5101 --- /dev/null +++ b/src/core/application/middlewares/returnValues.ts @@ -0,0 +1,4 @@ +export const UNAUTHORIZED_INVALID_TOKEN = { + message: "Unauthorized: Invalid token", + code: 401, +}