diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b41e8ac..12e500a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -77,8 +77,8 @@ model UserAddressDetails { cityXid Int @map("city_xid") cities Cities @relation(fields: [cityXid], references: [id], onDelete: Restrict) pinCode String @map("pin_code") - locationName String? @map("location_name") - locationAddress String? @map("location_address") + locationName String? @map("location_name") + locationAddress String? @map("location_address") locationLat Float? @map("location_lat") locationLong Float? @map("location_long") isActive Boolean @default(true) @map("is_active") @@ -124,20 +124,21 @@ model UserOtp { } model InviteDetails { - id Int @id @default(autoincrement()) - userXid Int @map("user_xid") - user User @relation("InvitedUser", fields: [userXid], references: [id], onDelete: Cascade) - is_invited Boolean @default(false) @map("is_invited") - invited_by Int @map("invited_by") - invitedBy User @relation("InviterUser", fields: [invited_by], references: [id], onDelete: Restrict) - invited_on DateTime @default(now()) @map("invited_on") - is_accepted Boolean @default(false) @map("is_accepted") - accepted_on DateTime? @map("accepted_on") - invitation_status String @default("pending") @map("invitation_status") - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") + id Int @id @default(autoincrement()) + userXid Int @map("user_xid") + user User @relation("InvitedUser", fields: [userXid], references: [id], onDelete: Cascade) + is_invited Boolean @default(false) @map("is_invited") + invited_by Int @map("invited_by") + invitedBy User @relation("InviterUser", fields: [invited_by], references: [id], onDelete: Restrict) + invited_on DateTime @default(now()) @map("invited_on") + is_accepted Boolean @default(false) @map("is_accepted") + accepted_on DateTime? @map("accepted_on") + invitation_status String @default("pending") @map("invitation_status") + isMinglarInvitation Boolean @default(false) @map("is_minglar_invitation") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") @@map("invite_details") @@schema("usr") diff --git a/serverless.yml b/serverless.yml index d97eefc..8440a46 100644 --- a/serverless.yml +++ b/serverless.yml @@ -306,6 +306,22 @@ functions: - httpApi: path: /minglaradmin/invite-teammate method: post + + + getAllInvitationDetails: + handler: src/modules/minglaradmin/handlers/getAllInvitationDetails.handler + package: + patterns: + - "src/modules/minglaradmin/**" + - "common/**" + - "src/common/**" + - "node_modules/@prisma/client/**" + - "node_modules/.prisma/**" + + events: + - httpApi: + path: /minglaradmin/get-all-invitation-details + method: get addCompanyDetails: diff --git a/src/common/middlewares/jwt/authForOnlyMinglarAdmin.ts b/src/common/middlewares/jwt/authForOnlyMinglarAdmin.ts new file mode 100644 index 0000000..9bf6f8e --- /dev/null +++ b/src/common/middlewares/jwt/authForOnlyMinglarAdmin.ts @@ -0,0 +1,118 @@ +import jwt from 'jsonwebtoken'; +import httpStatus from 'http-status'; +import { Request, Response, NextFunction } from 'express'; +import { PrismaClient } from '@prisma/client'; +import ApiError from '../../utils/helper/ApiError'; +import config from '../../../config/config'; +import { ROLE } from '@/common/utils/constants/common.constant'; + +const prisma = new PrismaClient(); + +interface DecodedToken { + id?: number; + sub?: string | number; + role?: string; + iat: number; + exp: number; +} + +interface UserPayload { + id: string; + role?: string; +} + +declare module 'express-serve-static-core' { + interface Request { + user?: UserPayload; + } +} + +/** + * Core authentication function - verifies JWT and validates Host user + * Can be used by both Express middleware and Lambda handlers + */ +export async function verifyOnlyMinglarAdminToken(token: string): Promise<{ id: number; role?: string }> { + if (!token) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'); + } + + try { + const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken; + + const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null); + + if (!userId) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload'); + } + + // ✅ Fetch user from Prisma (Host user only) + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { role: true }, + }); + + if (!user) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found'); + } + + // ✅ Check if user is active + if (user.isActive === false) { + throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.'); + } + + // ✅ Check Minglar Roles (role_xid = 1) + if (user.roleXid !== ROLE.MINGLAR_ADMIN) { + throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.'); + } + + + return { id: user.id, role: user.role?.roleName }; + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.'); + } + + if (error instanceof ApiError) { + throw error; + } + + throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.'); + } +} + +/** + * Verifies JWT and validates Host user (role_xid = 1) + */ +const verifyCallback = async ( + req: Request, + resolve: (value?: unknown) => void, + reject: (reason?: Error) => void +) => { + const token = req.header('x-auth-token') || req.cookies?.accessToken; + + try { + const userInfo = await verifyOnlyMinglarAdminToken(token); + + // Attach user to request + req.user = { id: userInfo.id.toString(), role: userInfo.role }; + + resolve(); + } catch (error) { + return reject(error as Error); + } +}; + +/** + * Express middleware — use as `auth()` in routes + */ +const authForHost = + () => + async (req: Request, res: Response, next: NextFunction) => { + return new Promise((resolve, reject) => { + verifyCallback(req, resolve, reject); + }) + .then(() => next()) + .catch((err) => next(err)); + }; + +export default authForHost; diff --git a/src/common/utils/constants/minglar.constant.ts b/src/common/utils/constants/minglar.constant.ts index b558e1d..93b2f51 100644 --- a/src/common/utils/constants/minglar.constant.ts +++ b/src/common/utils/constants/minglar.constant.ts @@ -20,4 +20,5 @@ export const MINGLAR_INVITATION_STATUS = { PENDING: "Pending", ACCEPTED: "Accepted", REJECTED: "Rejected", + INVITED: "Invited", } \ No newline at end of file diff --git a/src/modules/minglaradmin/handlers/getAllInvitationDetails.ts b/src/modules/minglaradmin/handlers/getAllInvitationDetails.ts new file mode 100644 index 0000000..f00bf88 --- /dev/null +++ b/src/modules/minglaradmin/handlers/getAllInvitationDetails.ts @@ -0,0 +1,39 @@ +import { verifyOnlyMinglarAdminToken } from '@/common/middlewares/jwt/authForOnlyMinglarAdmin'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { PrismaService } from '../../../common/database/prisma.service'; +import { safeHandler } from '../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../common/utils/helper/ApiError'; +import { MinglarService } from '../services/minglar.service'; + +const prismaService = new PrismaService(); +const minglarService = new MinglarService(prismaService); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Extract token from headers + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'] + if(!token) { + throw new ApiError(400, 'This is a protected route. Please provide a valid token.'); + } + + // Authenticate user using the shared authForHost function + await verifyOnlyMinglarAdminToken(token); + + const result = await minglarService.getAllInvitationDetails(); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Data retrieved successfully', + data: result, + }), + }; +}); + diff --git a/src/modules/minglaradmin/handlers/inviteTeammate.ts b/src/modules/minglaradmin/handlers/inviteTeammate.ts index face527..71ba60b 100644 --- a/src/modules/minglaradmin/handlers/inviteTeammate.ts +++ b/src/modules/minglaradmin/handlers/inviteTeammate.ts @@ -106,7 +106,7 @@ export const handler = safeHandler(async ( await minglarService.createUserRevenue(user.id, isFixedSalary, perValue || 0); // Create invite details - using service - await minglarService.createInviteDetails(user.id, adminUser.id, MINGLAR_INVITATION_STATUS.PENDING); + await minglarService.createInviteDetails(user.id, adminUser.id, MINGLAR_INVITATION_STATUS.INVITED); await sendInvitationEmailForMinglarAdmin(emailAddress); diff --git a/src/modules/minglaradmin/services/minglar.service.ts b/src/modules/minglaradmin/services/minglar.service.ts index c62cbf3..9afd453 100644 --- a/src/modules/minglaradmin/services/minglar.service.ts +++ b/src/modules/minglaradmin/services/minglar.service.ts @@ -329,5 +329,32 @@ export class MinglarService { }); } + async getAllInvitationDetails() { + return await this.prisma.inviteDetails.findMany({ + where: { + isMinglarInvitation: true, + isActive: true, + }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + emailAddress: true, + mobileNumber: true, + roleXid: true, + role: { + select: { + id: true, + roleName: true, + } + } + } + } + } + }) + } + }