diff --git a/bun.lockb b/bun.lockb index d0482c7..e4fd7e6 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f94ba05..cfd7221 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,11 @@ }, "dependencies": { "@prisma/client": "^6.7.0", - "elysia": "latest" + "@types/jsonwebtoken": "^9.0.9", + "cookie": "^1.0.2", + "elysia": "latest", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2" }, "devDependencies": { "bun-types": "latest", diff --git a/src/utils/callback/httpResponse.ts b/src/helpers/callback/httpResponse.ts similarity index 100% rename from src/utils/callback/httpResponse.ts rename to src/helpers/callback/httpResponse.ts diff --git a/src/helpers/jwt/decodeToken/index.ts b/src/helpers/jwt/decodeToken/index.ts new file mode 100644 index 0000000..cbafd46 --- /dev/null +++ b/src/helpers/jwt/decodeToken/index.ts @@ -0,0 +1,46 @@ +import jwt from "jsonwebtoken"; +import { Context } from "elysia"; +import { JWTAuthToken } from "./types"; +import { returnErrorResponse } from "../../callback/httpResponse"; +import { parse } from "cookie"; + +/** + * Verifies the authentication cookie from the request header. + * + * This helper function is used in an ElysiaJS context to check the validity of + * a user's authentication token stored in cookies. If the cookie is not found, + * it returns a `400 Bad Request`. If the token is invalid or expired, it returns + * a `401 Unauthorized`. If the token is valid, it returns the decoded user data. + * + * @param ctx - The request context from Elysia, used to read headers and set the response. + * + * @returns The decoded JWT payload if the token is valid, + * or a standardized error response if the cookie is missing or invalid. + * + * @example + * const decodedToken = decodeAuthToken(ctx); + * ctx => Elysia context + */ +export const JWTDecodeToken = (ctx: Context): JWTAuthToken => { + const cookiePayload = ctx.request.headers.get("Cookie"); + if (!cookiePayload) + throw returnErrorResponse(ctx.set, 400, "Bad Request", "No cookies found"); + + const cookies = parse(cookiePayload); + const cookiesToken = cookies.auth_token!; + + try { + const decodedToken = jwt.verify( + cookiesToken, + process.env.JWT_SECRET! + ) as JWTAuthToken; + return decodedToken; + } catch (error) { + throw returnErrorResponse( + ctx.set, + 401, + "Unauthorized", + "Invalid or expired token" + ); + } +}; diff --git a/src/helpers/jwt/decodeToken/types.ts b/src/helpers/jwt/decodeToken/types.ts new file mode 100644 index 0000000..3d094f0 --- /dev/null +++ b/src/helpers/jwt/decodeToken/types.ts @@ -0,0 +1,62 @@ +export interface JWTAuthToken { + id: string; + isAuthenticated: boolean; + userId: string; + deviceType: string; + deviceOs: string; + deviceIp: string; + isOnline: boolean; + lastOnline: Date; + validUntil: Date; + deletedAt: null; + createdAt: Date; + updatedAt: Date; + user: User; + iat: number; + exp: number; +} + +interface User { + id: string; + name: string; + username: string; + email: string; + birthDate: null; + gender: null; + phoneCC: null; + phoneNumber: null; + bioProfile: null; + profilePicture: null; + commentPicture: null; + preferenceId: null; + verifiedAt: null; + disabledAt: null; + deletedAt: null; + createdAt: Date; + updatedAt: Date; + roles: Role[]; +} + +interface Role { + id: string; + name: string; + primaryColor: string; + secondaryColor: string; + pictureImage: string; + badgeImage: null; + isSuperadmin: boolean; + canEditMedia: boolean; + canManageMedia: boolean; + canEditEpisodes: boolean; + canManageEpisodes: boolean; + canEditComment: boolean; + canManageComment: boolean; + canEditUser: boolean; + canManageUser: boolean; + canEditSystem: boolean; + canManageSystem: boolean; + createdBy: string; + deletedAt: null; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/modules/userRole/controller/createUserRole.controller.ts b/src/modules/userRole/controller/createUserRole.controller.ts index dfdc25b..0e398aa 100644 --- a/src/modules/userRole/controller/createUserRole.controller.ts +++ b/src/modules/userRole/controller/createUserRole.controller.ts @@ -1,5 +1,80 @@ +import { Prisma } from "@prisma/client"; import { Context } from "elysia"; +import { + returnErrorResponse, + returnWriteResponse, +} from "../../../helpers/callback/httpResponse"; +import { handlePrismaError } from "../../../utils/databases/prisma/error/handler"; +import { createUserRoleSchema } from "../userRole.schema"; +import { JWTDecodeToken } from "../../../helpers/jwt/decodeToken"; +import { prisma } from "../../../utils/databases/prisma/connection"; +import { createUserRoleService } from "../services/createUserRole.service"; -export const createUserRole = (ctx: Context) => { - return "Hello User Role Module"; +/** + * @function createUserRole + * @description Creates a new user role in the database. + * + * @param {Context & { body: UserRole }} ctx - The context object containing the request body. + * @param {UserRole} ctx.body - The user role data to be created. + * + * @returns {Promise} A response object indicating success or failure. + * @throws {Object} An error response object if validation fails or an error occurs during role creation. + * + * @example + * Request route: POST /roles + * Request body: + * { + * "userID": "e31668e6-c261-4a7e-9469-ffad734cf2dd", + * "name": "Admin", + * "primaryColor": "#D9D9D9", + * "secondaryColor": "#FFFFFF", + * "pictureImage": "https://example.com/picture.jpg", + * "badgeImage": "https://example.com/badge.jpg", + * "isSuperadmin": false, + * "canEditMedia": false, + * "canManageMedia": false, + * "canEditEpisodes": false, + * "canManageEpisodes": false, + * "canEditComment": false, + * "canManageComment": false, + * "canEditUser": false, + * "canManageUser": false, + * "canEditSystem": false, + * "canManageSystem": false + * } + */ +export const createUserRole = async ( + ctx: Context & { body: Prisma.UserRoleUncheckedCreateInput } +) => { + const { error } = createUserRoleSchema.validate(ctx.body); + if (error) + return returnErrorResponse(ctx.set, 400, "Invalid user input", error); + + const formData: Prisma.UserRoleUncheckedCreateInput = { ...ctx.body }; + const userCreator = JWTDecodeToken(ctx); + + const dataPayload = { + ...formData, + isSuperadmin: Boolean(formData.isSuperadmin), + canEditMedia: Boolean(formData.canEditMedia), + canManageMedia: Boolean(formData.canManageMedia), + canEditEpisodes: Boolean(formData.canEditEpisodes), + canManageEpisodes: Boolean(formData.canManageEpisodes), + canEditComment: Boolean(formData.canEditComment), + canManageComment: Boolean(formData.canManageComment), + canEditUser: Boolean(formData.canEditUser), + canManageUser: Boolean(formData.canManageUser), + canEditSystem: Boolean(formData.canEditSystem), + canManageSystem: Boolean(formData.canManageSystem), + createdBy: userCreator.user.id, + deletedAt: null, + }; + + createUserRoleService(dataPayload) + .then((result) => + returnWriteResponse(ctx.set, 201, "User role created", result) + ) + .catch((error) => + returnErrorResponse(ctx.set, 500, "Internal Server Error", error) + ); }; diff --git a/src/modules/userRole/services/createUserRole.service.ts b/src/modules/userRole/services/createUserRole.service.ts index e69de29..134738a 100644 --- a/src/modules/userRole/services/createUserRole.service.ts +++ b/src/modules/userRole/services/createUserRole.service.ts @@ -0,0 +1,20 @@ +import { Prisma } from "@prisma/client"; +import { userRoleModel } from "../userRole.model"; +import { handlePrismaError } from "../../../utils/databases/prisma/error/handler"; +import { returnErrorResponse } from "../../../helpers/callback/httpResponse"; +import { Context } from "elysia"; + +export const createUserRoleService = async ( + ctx: Context, + userRoleData: Prisma.UserRoleUncheckedCreateInput +) => { + try { + const newUserRole = await userRoleModel.create({ + data: userRoleData, + }); + return newUserRole; + } catch (error) { + const { status, message, details } = handlePrismaError(error); + throw returnErrorResponse(ctx.set, status, message, details); + } +}; diff --git a/src/modules/userRole/userRole.model.ts b/src/modules/userRole/userRole.model.ts index e69de29..8b2d986 100644 --- a/src/modules/userRole/userRole.model.ts +++ b/src/modules/userRole/userRole.model.ts @@ -0,0 +1,3 @@ +import { prisma } from "../../utils/databases/prisma/connection"; + +export const userRoleModel = prisma.userRole; diff --git a/src/modules/userRole/userRole.schema.ts b/src/modules/userRole/userRole.schema.ts index e69de29..1a9308d 100644 --- a/src/modules/userRole/userRole.schema.ts +++ b/src/modules/userRole/userRole.schema.ts @@ -0,0 +1,28 @@ +import Joi from "joi"; + +export const createUserRoleSchema = Joi.object({ + name: Joi.string().min(4).max(255).required(), + primaryColor: Joi.string() + .pattern(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/) + .optional(), + secondaryColor: Joi.string() + .pattern(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/) + .optional(), + pictureImage: Joi.string() + .uri({ scheme: ["http", "https"] }) + .optional(), + badgeImage: Joi.string() + .uri({ scheme: ["http", "https"] }) + .optional(), + isSuperadmin: Joi.boolean().required(), + canEditMedia: Joi.boolean().required(), + canManageMedia: Joi.boolean().required(), + canEditEpisodes: Joi.boolean().required(), + canManageEpisodes: Joi.boolean().required(), + canEditComment: Joi.boolean().required(), + canManageComment: Joi.boolean().required(), + canEditUser: Joi.boolean().required(), + canManageUser: Joi.boolean().required(), + canEditSystem: Joi.boolean().required(), + canManageSystem: Joi.boolean().required(), +});