made the role based access system for the host panel
This commit is contained in:
@@ -64,6 +64,8 @@ model User {
|
||||
paymentOrders PaymentOrders[]
|
||||
inviteDetails InviteDetails[] @relation("InvitedUser")
|
||||
invitedInviteDetails InviteDetails[] @relation("InviterUser")
|
||||
hostMembers HostMembers[] @relation("HostMemberUser")
|
||||
invitedHostMembers HostMembers[] @relation("HostMemberInviter")
|
||||
userRevenues UserRevenue[]
|
||||
userInterests UserInterests[]
|
||||
connectDetails ConnectDetails[]
|
||||
@@ -677,6 +679,8 @@ model Roles {
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
User User[]
|
||||
hostMembers HostMembers[] @relation("HostMemberRole")
|
||||
hostRolePermissionMasters HostRolePermissionMasters[] @relation("HostRolePermissionMasterRole")
|
||||
|
||||
@@map("roles")
|
||||
@@schema("mst")
|
||||
@@ -809,6 +813,8 @@ model HostHeader {
|
||||
HostBankDetails HostBankDetails[]
|
||||
HostDocuments HostDocuments[]
|
||||
HostSuggestion HostSuggestion[]
|
||||
hostMembers HostMembers[]
|
||||
hostRolePermissionMasters HostRolePermissionMasters[]
|
||||
hostParent HostParent[]
|
||||
HostTrack HostTrack[]
|
||||
Activities Activities[]
|
||||
@@ -840,13 +846,90 @@ model HostBankDetails {
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostMembers {
|
||||
id Int @id @default(autoincrement())
|
||||
hostXid Int @map("host_xid")
|
||||
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
|
||||
userXid Int @map("user_xid")
|
||||
user User @relation("HostMemberUser", fields: [userXid], references: [id], onDelete: Cascade)
|
||||
roleXid Int @map("role_xid")
|
||||
role Roles @relation("HostMemberRole", fields: [roleXid], references: [id], onDelete: Restrict)
|
||||
hostRolePermissionMasterXid Int? @map("host_role_permission_master_xid")
|
||||
hostRolePermissionMaster HostRolePermissionMasters? @relation(fields: [hostRolePermissionMasterXid], references: [id], onDelete: Restrict)
|
||||
memberStatus String @default("invited") @map("member_status") @db.VarChar(20)
|
||||
invitedByXid Int? @map("invited_by_xid")
|
||||
invitedBy User? @relation("HostMemberInviter", fields: [invitedByXid], references: [id], onDelete: Restrict)
|
||||
invitedOn DateTime @default(now()) @map("invited_on")
|
||||
acceptedOn DateTime? @map("accepted_on")
|
||||
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")
|
||||
managedActivities HostMemberActivities[]
|
||||
|
||||
@@unique([hostXid, userXid])
|
||||
@@map("host_members")
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostRolePermissionMasters {
|
||||
id Int @id @default(autoincrement())
|
||||
hostXid Int @map("host_xid")
|
||||
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
|
||||
roleXid Int @map("role_xid")
|
||||
role Roles @relation("HostRolePermissionMasterRole", fields: [roleXid], references: [id], onDelete: Restrict)
|
||||
permissionMasterXids Json @map("permission_master_xids")
|
||||
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")
|
||||
hostMembers HostMembers[]
|
||||
|
||||
@@unique([hostXid, roleXid])
|
||||
@@map("host_role_permission_masters")
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostPermissionMasters {
|
||||
id Int @id @default(autoincrement())
|
||||
permissionKey String @unique @map("permission_key") @db.VarChar(120)
|
||||
permissionGroup String @map("permission_group") @db.VarChar(80)
|
||||
permissionSection String @map("permission_section") @db.VarChar(80)
|
||||
permissionAction String @map("permission_action") @db.VarChar(20)
|
||||
displayLabel String @map("display_label") @db.VarChar(120)
|
||||
displayOrder Int @map("display_order")
|
||||
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("host_permission_masters")
|
||||
@@schema("mst")
|
||||
}
|
||||
|
||||
model HostMemberActivities {
|
||||
id Int @id @default(autoincrement())
|
||||
hostMemberXid Int @map("host_member_xid")
|
||||
hostMember HostMembers @relation(fields: [hostMemberXid], references: [id], onDelete: Cascade)
|
||||
activityXid Int @map("activity_xid")
|
||||
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
|
||||
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")
|
||||
|
||||
@@unique([hostMemberXid, activityXid])
|
||||
@@map("host_member_activities")
|
||||
@@schema("hst")
|
||||
}
|
||||
|
||||
model HostDocuments {
|
||||
id Int @id @default(autoincrement())
|
||||
hostXid Int @map("host_xid")
|
||||
host HostHeader @relation(fields: [hostXid], references: [id], onDelete: Cascade)
|
||||
documentTypeXid Int @map("document_type_xid")
|
||||
documentType DocumentType @relation(fields: [documentTypeXid], references: [id], onDelete: Restrict)
|
||||
documentName String @map("document_name") @db.VarChar(20)
|
||||
documentName String @map("document_name") @db.VarChar(50)
|
||||
filePath String @map("file_path") @db.VarChar(400)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -1056,6 +1139,7 @@ model Activities {
|
||||
activityPickUpTransports ActivityPickUpTransport[]
|
||||
userBucketInterests UserBucketInterested[]
|
||||
activityMessages ActivityMessages[]
|
||||
assignedHostMembers HostMemberActivities[]
|
||||
|
||||
@@map("activities")
|
||||
@@schema("act")
|
||||
|
||||
@@ -7,7 +7,73 @@ const prisma = new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
||||
});
|
||||
|
||||
const HOST_PERMISSION_MASTER_SEED = [
|
||||
{ permissionKey: 'profile.company_profile.edit', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'Edit', displayLabel: 'Company Profile - Edit', displayOrder: 1 },
|
||||
{ permissionKey: 'profile.company_profile.view', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'View', displayLabel: 'Company Profile - View', displayOrder: 2 },
|
||||
{ permissionKey: 'profile.company_profile.hide', permissionGroup: 'Profile', permissionSection: 'Company Profile', permissionAction: 'Hide', displayLabel: 'Company Profile - Hide', displayOrder: 3 },
|
||||
{ permissionKey: 'activity_management.onboarding.edit', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'Edit', displayLabel: 'Onboarding - Edit', displayOrder: 4 },
|
||||
{ permissionKey: 'activity_management.onboarding.view', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'View', displayLabel: 'Onboarding - View', displayOrder: 5 },
|
||||
{ permissionKey: 'activity_management.onboarding.hide', permissionGroup: 'Activity Management', permissionSection: 'Onboarding', permissionAction: 'Hide', displayLabel: 'Onboarding - Hide', displayOrder: 6 },
|
||||
{ permissionKey: 'activity_management.scheduling.edit', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'Edit', displayLabel: 'Scheduling - Edit', displayOrder: 7 },
|
||||
{ permissionKey: 'activity_management.scheduling.view', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'View', displayLabel: 'Scheduling - View', displayOrder: 8 },
|
||||
{ permissionKey: 'activity_management.scheduling.hide', permissionGroup: 'Activity Management', permissionSection: 'Scheduling', permissionAction: 'Hide', displayLabel: 'Scheduling - Hide', displayOrder: 9 },
|
||||
{ permissionKey: 'activity_management.reservation.edit', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'Edit', displayLabel: 'Reservation - Edit', displayOrder: 10 },
|
||||
{ permissionKey: 'activity_management.reservation.view', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'View', displayLabel: 'Reservation - View', displayOrder: 11 },
|
||||
{ permissionKey: 'activity_management.reservation.hide', permissionGroup: 'Activity Management', permissionSection: 'Reservation', permissionAction: 'Hide', displayLabel: 'Reservation - Hide', displayOrder: 12 },
|
||||
{ permissionKey: 'analytics_statements.revenue_statistics.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'Edit', displayLabel: 'Revenue Statistics - Edit', displayOrder: 13 },
|
||||
{ permissionKey: 'analytics_statements.revenue_statistics.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'View', displayLabel: 'Revenue Statistics - View', displayOrder: 14 },
|
||||
{ permissionKey: 'analytics_statements.revenue_statistics.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Revenue Statistics', permissionAction: 'Hide', displayLabel: 'Revenue Statistics - Hide', displayOrder: 15 },
|
||||
{ permissionKey: 'analytics_statements.technical_statistics.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'Edit', displayLabel: 'Technical Statistics - Edit', displayOrder: 16 },
|
||||
{ permissionKey: 'analytics_statements.technical_statistics.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'View', displayLabel: 'Technical Statistics - View', displayOrder: 17 },
|
||||
{ permissionKey: 'analytics_statements.technical_statistics.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Technical Statistics', permissionAction: 'Hide', displayLabel: 'Technical Statistics - Hide', displayOrder: 18 },
|
||||
{ permissionKey: 'analytics_statements.reservation.edit', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'Edit', displayLabel: 'Reservation - Edit', displayOrder: 19 },
|
||||
{ permissionKey: 'analytics_statements.reservation.view', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'View', displayLabel: 'Reservation - View', displayOrder: 20 },
|
||||
{ permissionKey: 'analytics_statements.reservation.hide', permissionGroup: 'Analytics & Statements', permissionSection: 'Reservation', permissionAction: 'Hide', displayLabel: 'Reservation - Hide', displayOrder: 21 },
|
||||
{ permissionKey: 'communication.messages.edit', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'Edit', displayLabel: 'Messages - Edit', displayOrder: 22 },
|
||||
{ permissionKey: 'communication.messages.view', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'View', displayLabel: 'Messages - View', displayOrder: 23 },
|
||||
{ permissionKey: 'communication.messages.hide', permissionGroup: 'Communication', permissionSection: 'Messages', permissionAction: 'Hide', displayLabel: 'Messages - Hide', displayOrder: 24 },
|
||||
{ permissionKey: 'communication.broadcast.edit', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'Edit', displayLabel: 'Broadcast - Edit', displayOrder: 25 },
|
||||
{ permissionKey: 'communication.broadcast.view', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'View', displayLabel: 'Broadcast - View', displayOrder: 26 },
|
||||
{ permissionKey: 'communication.broadcast.hide', permissionGroup: 'Communication', permissionSection: 'Broadcast', permissionAction: 'Hide', displayLabel: 'Broadcast - Hide', displayOrder: 27 },
|
||||
{ permissionKey: 'promotions.creating_new_promotions.edit', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'Edit', displayLabel: 'Creating New Promotions - Edit', displayOrder: 28 },
|
||||
{ permissionKey: 'promotions.creating_new_promotions.view', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'View', displayLabel: 'Creating New Promotions - View', displayOrder: 29 },
|
||||
{ permissionKey: 'promotions.creating_new_promotions.hide', permissionGroup: 'Promotions', permissionSection: 'Creating New Promotions', permissionAction: 'Hide', displayLabel: 'Creating New Promotions - Hide', displayOrder: 30 },
|
||||
{ permissionKey: 'promotions.view_promotions.edit', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'Edit', displayLabel: 'View Promotions - Edit', displayOrder: 31 },
|
||||
{ permissionKey: 'promotions.view_promotions.view', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'View', displayLabel: 'View Promotions - View', displayOrder: 32 },
|
||||
{ permissionKey: 'promotions.view_promotions.hide', permissionGroup: 'Promotions', permissionSection: 'View Promotions', permissionAction: 'Hide', displayLabel: 'View Promotions - Hide', displayOrder: 33 },
|
||||
{ permissionKey: 'user_management.inviting_new_users.edit', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'Edit', displayLabel: 'Inviting New Users - Edit', displayOrder: 34 },
|
||||
{ permissionKey: 'user_management.inviting_new_users.view', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'View', displayLabel: 'Inviting New Users - View', displayOrder: 35 },
|
||||
{ permissionKey: 'user_management.inviting_new_users.hide', permissionGroup: 'User Management', permissionSection: 'Inviting New Users', permissionAction: 'Hide', displayLabel: 'Inviting New Users - Hide', displayOrder: 36 },
|
||||
];
|
||||
|
||||
async function seedHostPermissionMasters() {
|
||||
for (const permission of HOST_PERMISSION_MASTER_SEED) {
|
||||
await prisma.hostPermissionMasters.upsert({
|
||||
where: { permissionKey: permission.permissionKey },
|
||||
update: {
|
||||
permissionGroup: permission.permissionGroup,
|
||||
permissionSection: permission.permissionSection,
|
||||
permissionAction: permission.permissionAction,
|
||||
displayLabel: permission.displayLabel,
|
||||
displayOrder: permission.displayOrder,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
create: {
|
||||
permissionKey: permission.permissionKey,
|
||||
permissionGroup: permission.permissionGroup,
|
||||
permissionSection: permission.permissionSection,
|
||||
permissionAction: permission.permissionAction,
|
||||
displayLabel: permission.displayLabel,
|
||||
displayOrder: permission.displayOrder,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await seedHostPermissionMasters();
|
||||
// ✅ Countries
|
||||
// const india = await prisma.countries.upsert({
|
||||
// where: { countryName: 'India' },
|
||||
|
||||
@@ -308,6 +308,54 @@ updateHostProfile:
|
||||
path: /profile
|
||||
method: patch
|
||||
|
||||
inviteHostMember:
|
||||
handler: src/modules/host/handlers/settings/inviteMember.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/invite-member
|
||||
method: post
|
||||
|
||||
saveRolePermissions:
|
||||
handler: src/modules/host/handlers/settings/saveRolePermissions.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/save-role-permissions
|
||||
method: post
|
||||
|
||||
getPermissionMasters:
|
||||
handler: src/modules/host/handlers/settings/getPermissionMasters.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/settings/**'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /settings/permission-masters
|
||||
method: get
|
||||
|
||||
# Functions with S3/AWS SDK dependencies
|
||||
submitCompanyDetails:
|
||||
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
|
||||
|
||||
60
src/modules/host/handlers/settings/getPermissionMasters.ts
Normal file
60
src/modules/host/handlers/settings/getPermissionMasters.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
await verifyHostToken(token);
|
||||
|
||||
const permissionMasters = await prismaClient.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionKey: true,
|
||||
permissionGroup: true,
|
||||
permissionSection: true,
|
||||
permissionAction: true,
|
||||
displayLabel: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Permission masters fetched successfully',
|
||||
data: {
|
||||
permissionMasters,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
111
src/modules/host/handlers/settings/inviteMember.ts
Normal file
111
src/modules/host/handlers/settings/inviteMember.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { HostMemberService } from '../../services/hostMember.service';
|
||||
import { sendHostMemberInvitationEmail } from '../../services/sendHostMemberInvitationEmail.service';
|
||||
|
||||
const hostMemberService = new HostMemberService(prismaClient);
|
||||
|
||||
interface InviteMemberBody {
|
||||
emailAddress: string;
|
||||
roleXid: number;
|
||||
permissionMasterXid: number;
|
||||
activityXids: number[];
|
||||
}
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
const userInfo = await verifyHostToken(token);
|
||||
|
||||
let body: Partial<InviteMemberBody> = {};
|
||||
if (event.body) {
|
||||
try {
|
||||
body = JSON.parse(event.body);
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON body');
|
||||
}
|
||||
}
|
||||
|
||||
const emailAddress =
|
||||
typeof body.emailAddress === 'string' ? body.emailAddress.trim() : '';
|
||||
const roleXid = Number(body.roleXid);
|
||||
const permissionMasterXid = Number(body.permissionMasterXid);
|
||||
const activityXids = Array.isArray(body.activityXids)
|
||||
? body.activityXids
|
||||
: [];
|
||||
|
||||
if (!emailAddress) {
|
||||
throw new ApiError(400, 'emailAddress is required.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(roleXid) || roleXid <= 0) {
|
||||
throw new ApiError(400, 'roleXid is required.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(permissionMasterXid) || permissionMasterXid <= 0) {
|
||||
throw new ApiError(400, 'permissionMasterXid is required.');
|
||||
}
|
||||
|
||||
if (!activityXids.length) {
|
||||
throw new ApiError(400, 'activityXids is required.');
|
||||
}
|
||||
|
||||
const inviteResult = await hostMemberService.inviteMember({
|
||||
inviterUserXid: userInfo.id,
|
||||
emailAddress,
|
||||
roleXid,
|
||||
permissionMasterXid,
|
||||
activityXids,
|
||||
});
|
||||
|
||||
await sendHostMemberInvitationEmail(
|
||||
inviteResult.user.emailAddress ?? emailAddress,
|
||||
inviteResult.host.companyName,
|
||||
inviteResult.permissionMaster.role.roleName,
|
||||
inviteResult.permissionDetails.map((permission) => permission.displayLabel),
|
||||
inviteResult.activities.map((activity) => activity.activityTitle ?? `Activity #${activity.id}`),
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Host member invited successfully',
|
||||
data: {
|
||||
hostMemberId: inviteResult.hostMember.id,
|
||||
hostXid: inviteResult.hostMember.hostXid,
|
||||
userXid: inviteResult.hostMember.userXid,
|
||||
emailAddress: inviteResult.user.emailAddress,
|
||||
roleXid: inviteResult.hostMember.roleXid,
|
||||
permissionMasterXid: inviteResult.hostMember.hostRolePermissionMasterXid,
|
||||
permissionMasterXids: inviteResult.permissionMaster.permissionMasterXids,
|
||||
permissionLabels: inviteResult.permissionDetails.map((permission) => permission.displayLabel),
|
||||
activityXids: inviteResult.activities.map((activity) => activity.id),
|
||||
activityNames: inviteResult.activities.map((activity) => activity.activityTitle ?? null),
|
||||
memberStatus: inviteResult.hostMember.memberStatus,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
81
src/modules/host/handlers/settings/saveRolePermissions.ts
Normal file
81
src/modules/host/handlers/settings/saveRolePermissions.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
APIGatewayProxyEvent,
|
||||
APIGatewayProxyResult,
|
||||
Context,
|
||||
} from 'aws-lambda';
|
||||
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { HostRolePermissionService } from '../../services/hostRolePermission.service';
|
||||
|
||||
const hostRolePermissionService = new HostRolePermissionService(prismaClient);
|
||||
|
||||
interface SaveRolePermissionsBody {
|
||||
roleXid: number;
|
||||
permissionMasterXids: number[];
|
||||
}
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
|
||||
const userInfo = await verifyHostToken(token);
|
||||
|
||||
let body: Partial<SaveRolePermissionsBody> = {};
|
||||
if (event.body) {
|
||||
try {
|
||||
body = JSON.parse(event.body);
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON body');
|
||||
}
|
||||
}
|
||||
|
||||
const roleXid = Number(body.roleXid);
|
||||
const permissionMasterXids = Array.isArray(body.permissionMasterXids)
|
||||
? body.permissionMasterXids
|
||||
: [];
|
||||
|
||||
if (!Number.isInteger(roleXid) || roleXid <= 0) {
|
||||
throw new ApiError(400, 'roleXid is required.');
|
||||
}
|
||||
|
||||
if (!permissionMasterXids.length) {
|
||||
throw new ApiError(400, 'permissionMasterXids is required.');
|
||||
}
|
||||
|
||||
const result = await hostRolePermissionService.saveRolePermissions({
|
||||
hostUserXid: userInfo.id,
|
||||
roleXid,
|
||||
permissionMasterXids,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Role permissions saved successfully',
|
||||
data: {
|
||||
permissionMasterXid: result.saved.id,
|
||||
hostXid: result.saved.hostXid,
|
||||
roleXid: result.saved.roleXid,
|
||||
permissionMasterXids: result.saved.permissionMasterXids,
|
||||
selectedPermissions: result.selectedPermissions,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
292
src/modules/host/services/hostMember.service.ts
Normal file
292
src/modules/host/services/hostMember.service.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { ROLE, USER_STATUS } from '../../../common/utils/constants/common.constant';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
|
||||
const ALLOWED_MEMBER_ROLES = new Set([ROLE.CO_ADMIN, ROLE.OPERATOR]);
|
||||
|
||||
function normalizeIdArray(values: unknown): number[] {
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isInteger(item) && item > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HostMemberService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async inviteMember(input: {
|
||||
inviterUserXid: number;
|
||||
emailAddress: string;
|
||||
roleXid: number;
|
||||
permissionMasterXid: number;
|
||||
activityXids: unknown;
|
||||
}) {
|
||||
const normalizedEmail = input.emailAddress.trim().toLowerCase();
|
||||
const roleXid = Number(input.roleXid);
|
||||
const permissionMasterXid = Number(input.permissionMasterXid);
|
||||
const activityXids = normalizeIdArray(input.activityXids);
|
||||
|
||||
if (!normalizedEmail) {
|
||||
throw new ApiError(400, 'emailAddress is required.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(roleXid) || !ALLOWED_MEMBER_ROLES.has(roleXid)) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'roleXid must be one of CO_ADMIN or OPERATOR.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(permissionMasterXid) || permissionMasterXid <= 0) {
|
||||
throw new ApiError(400, 'permissionMasterXid is required.');
|
||||
}
|
||||
|
||||
if (!activityXids.length) {
|
||||
throw new ApiError(400, 'At least one activity is required.');
|
||||
}
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const host = await tx.hostHeader.findFirst({
|
||||
where: {
|
||||
userXid: input.inviterUserXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
userXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
throw new ApiError(404, 'Host company not found for the logged-in user.');
|
||||
}
|
||||
|
||||
if (host.userXid !== input.inviterUserXid) {
|
||||
throw new ApiError(403, 'Only the host owner can invite members.');
|
||||
}
|
||||
|
||||
const permissionMaster = await tx.hostRolePermissionMasters.findFirst({
|
||||
where: {
|
||||
id: permissionMasterXid,
|
||||
hostXid: host.id,
|
||||
roleXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
roleXid: true,
|
||||
permissionMasterXids: true,
|
||||
role: {
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!permissionMaster) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
'Permission master not found for the selected host and role.',
|
||||
);
|
||||
}
|
||||
|
||||
const selectedPermissionMasterXids = normalizeIdArray(
|
||||
permissionMaster.permissionMasterXids,
|
||||
);
|
||||
|
||||
const permissionDetails = await tx.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
id: { in: selectedPermissionMasterXids },
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionKey: true,
|
||||
permissionGroup: true,
|
||||
permissionSection: true,
|
||||
permissionAction: true,
|
||||
displayLabel: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (permissionDetails.length !== selectedPermissionMasterXids.length) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'One or more saved permission XIDs no longer exist in the master table.',
|
||||
);
|
||||
}
|
||||
|
||||
const activities = await tx.activities.findMany({
|
||||
where: {
|
||||
id: { in: activityXids },
|
||||
hostXid: host.id,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (activities.length !== activityXids.length) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'One or more selected activities are invalid for this host.',
|
||||
);
|
||||
}
|
||||
|
||||
const existingUser = await tx.user.findFirst({
|
||||
where: {
|
||||
emailAddress: normalizedEmail,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emailAddress: true,
|
||||
roleXid: true,
|
||||
userStatus: true,
|
||||
},
|
||||
});
|
||||
|
||||
let user =
|
||||
existingUser ??
|
||||
(await tx.user.create({
|
||||
data: {
|
||||
emailAddress: normalizedEmail,
|
||||
roleXid: ROLE.HOST,
|
||||
userStatus: USER_STATUS.INVITED,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emailAddress: true,
|
||||
roleXid: true,
|
||||
userStatus: true,
|
||||
},
|
||||
}));
|
||||
|
||||
if (existingUser && existingUser.roleXid !== ROLE.HOST) {
|
||||
user = await tx.user.update({
|
||||
where: { id: existingUser.id },
|
||||
data: {
|
||||
roleXid: ROLE.HOST,
|
||||
userStatus: USER_STATUS.INVITED,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emailAddress: true,
|
||||
roleXid: true,
|
||||
userStatus: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const existingMembership = await tx.hostMembers.findFirst({
|
||||
where: {
|
||||
userXid: user.id,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMembership && existingMembership.hostXid !== host.id) {
|
||||
throw new ApiError(
|
||||
409,
|
||||
'This person is already invited or assigned to another host company.',
|
||||
);
|
||||
}
|
||||
|
||||
const membershipData = {
|
||||
hostXid: host.id,
|
||||
userXid: user.id,
|
||||
roleXid,
|
||||
hostRolePermissionMasterXid: permissionMaster.id,
|
||||
memberStatus: 'invited',
|
||||
invitedByXid: input.inviterUserXid,
|
||||
invitedOn: new Date(),
|
||||
acceptedOn: null,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
const hostMember = existingMembership
|
||||
? await tx.hostMembers.update({
|
||||
where: { id: existingMembership.id },
|
||||
data: membershipData,
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
userXid: true,
|
||||
roleXid: true,
|
||||
hostRolePermissionMasterXid: true,
|
||||
memberStatus: true,
|
||||
invitedByXid: true,
|
||||
invitedOn: true,
|
||||
},
|
||||
})
|
||||
: await tx.hostMembers.create({
|
||||
data: membershipData,
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
userXid: true,
|
||||
roleXid: true,
|
||||
hostRolePermissionMasterXid: true,
|
||||
memberStatus: true,
|
||||
invitedByXid: true,
|
||||
invitedOn: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.hostMemberActivities.deleteMany({
|
||||
where: {
|
||||
hostMemberXid: hostMember.id,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.hostMemberActivities.createMany({
|
||||
data: activities.map((activity) => ({
|
||||
hostMemberXid: hostMember.id,
|
||||
activityXid: activity.id,
|
||||
isActive: true,
|
||||
})),
|
||||
});
|
||||
|
||||
return {
|
||||
host,
|
||||
user,
|
||||
hostMember,
|
||||
permissionMaster,
|
||||
permissionDetails,
|
||||
activities,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
129
src/modules/host/services/hostRolePermission.service.ts
Normal file
129
src/modules/host/services/hostRolePermission.service.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
|
||||
function normalizeIdArray(values: unknown): number[] {
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isInteger(item) && item > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HostRolePermissionService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async saveRolePermissions(input: {
|
||||
hostUserXid: number;
|
||||
roleXid: number;
|
||||
permissionMasterXids: unknown;
|
||||
}) {
|
||||
const permissionMasterXids = normalizeIdArray(input.permissionMasterXids);
|
||||
|
||||
if (!permissionMasterXids.length) {
|
||||
throw new ApiError(400, 'permissionMasterXids is required.');
|
||||
}
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const host = await tx.hostHeader.findFirst({
|
||||
where: {
|
||||
userXid: input.hostUserXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
userXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
throw new ApiError(404, 'Host company not found for the logged-in user.');
|
||||
}
|
||||
|
||||
const role = await tx.roles.findFirst({
|
||||
where: {
|
||||
id: input.roleXid,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
roleName: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new ApiError(400, 'Invalid roleXid.');
|
||||
}
|
||||
|
||||
const selectedPermissions = await tx.hostPermissionMasters.findMany({
|
||||
where: {
|
||||
id: { in: permissionMasterXids },
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
permissionKey: true,
|
||||
permissionGroup: true,
|
||||
permissionSection: true,
|
||||
permissionAction: true,
|
||||
displayLabel: true,
|
||||
displayOrder: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (selectedPermissions.length !== permissionMasterXids.length) {
|
||||
throw new ApiError(400, 'One or more permissionMasterXids are invalid.');
|
||||
}
|
||||
|
||||
const saved = await tx.hostRolePermissionMasters.upsert({
|
||||
where: {
|
||||
hostXid_roleXid: {
|
||||
hostXid: host.id,
|
||||
roleXid: role.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
hostXid: host.id,
|
||||
roleXid: role.id,
|
||||
permissionMasterXids,
|
||||
isActive: true,
|
||||
},
|
||||
update: {
|
||||
permissionMasterXids,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
roleXid: true,
|
||||
permissionMasterXids: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
host,
|
||||
role,
|
||||
saved,
|
||||
selectedPermissions,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { brevoService } from '../../../common/email/brevoApi';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
|
||||
export async function sendHostMemberInvitationEmail(
|
||||
emailAddress: string,
|
||||
hostName: string,
|
||||
memberRole: string,
|
||||
permissionLabels: string[],
|
||||
activityNames: string[],
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
}> {
|
||||
const subject = `Invitation to join ${hostName} on Minglar Host`;
|
||||
|
||||
const permissionsHtml = permissionLabels.length
|
||||
? `<ul>${permissionLabels.map((permission) => `<li>${permission}</li>`).join('')}</ul>`
|
||||
: '<p>No permissions were assigned.</p>';
|
||||
|
||||
const activitiesHtml = activityNames.length
|
||||
? `<ul>${activityNames.map((activity) => `<li>${activity}</li>`).join('')}</ul>`
|
||||
: '<p>No activities were assigned.</p>';
|
||||
|
||||
const htmlContent = `
|
||||
<p>Hi there,</p>
|
||||
<p>You have been invited by <strong>${hostName}</strong> to join the Minglar Host portal as <strong>${memberRole}</strong>.</p>
|
||||
<p>The following permissions have been assigned to your account:</p>
|
||||
${permissionsHtml}
|
||||
<p>The following activities have been assigned to you:</p>
|
||||
${activitiesHtml}
|
||||
<p>You can access the host portal using the link below:</p>
|
||||
<p><a href="${config.HOST_LINK}" target="_blank">${config.HOST_LINK}</a></p>
|
||||
<p>If you were not expecting this invitation, you can ignore this email.</p>
|
||||
<p>Warm regards,<br/>Team Minglar</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
await brevoService.sendEmail({
|
||||
recipients: [{ email: emailAddress }],
|
||||
subject,
|
||||
htmlContent,
|
||||
});
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Brevo email send failed:', err);
|
||||
throw new ApiError(500, 'Failed to send host member invitation email.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user