Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/app/src/actions/organization/remove-employee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,18 @@ export const removeEmployeeRoleOrMember = authActionClient
};
}

// 3. Check if target has 'employee' role
// 3. Check if target has 'employee' or 'contractor' role
const roles = targetMember.role.split(',').filter(Boolean); // Handle empty strings/commas
if (!roles.includes('employee')) {
if (!roles.includes('employee') && !roles.includes('contractor')) {
return {
success: false,
error: 'Target member does not have the employee role.',
error: 'Target member does not have the employee or contractor role.',
};
}

// 4. Logic: Remove role or delete member
if (roles.length === 1 && roles[0] === 'employee') {
// Only has employee role - delete member fully
if (roles.length === 1 && (roles[0] === 'employee' || roles[0] === 'contractor')) {
// Only has employee or contractor role - delete member fully

// Cannot remove owner (shouldn't happen if only role is employee, but safety check)
if (targetMember.role === 'owner') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ export const acceptRequestedPolicyChangesAction = authActionClient
},
});

// Filter to get only employees
// Filter to get only employees and contractors
const employeeMembers = employees.filter((member) => {
const roles = member.role.includes(',') ? member.role.split(',') : [member.role];
return roles.includes('employee');
return roles.includes('employee') || roles.includes('contractor');
});

// Prepare the events array for the API
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/app/(app)/[orgId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default async function Layout({
return redirect('/auth/unauthorized');
}

if (member.role === 'employee') {
if (member.role === 'employee' || member.role === 'contractor') {
return redirect('/no-access');
}

Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ const getPoliciesTasks = async (employeeId: string) => {
where: {
organizationId: organizationId,
status: 'published',
isRequiredToSign: true,
isArchived: false,
},
orderBy: {
name: 'asc',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ import { addEmployeeWithoutInvite } from '../actions/addEmployeeWithoutInvite';
import { MultiRoleCombobox } from './MultiRoleCombobox';

// --- Constants for Roles ---
const selectableRoles = ['admin', 'auditor', 'employee'] as const satisfies Readonly<
const selectableRoles = ['admin', 'auditor', 'employee', 'contractor'] as const satisfies Readonly<
[Role, ...Role[]]
>;
type InviteRole = (typeof selectableRoles)[number];
const DEFAULT_ROLES: InviteRole[] = [];

// Type guard to check if a string is a valid InviteRole
const isInviteRole = (role: string): role is InviteRole => {
return role === 'admin' || role === 'auditor' || role === 'employee';
return role === 'admin' || role === 'auditor' || role === 'employee' || role === 'contractor';
};

// --- Schemas ---
Expand Down Expand Up @@ -159,7 +159,8 @@ export function InviteMembersModal({
// Process each invitation sequentially
for (const invite of values.manualInvites) {
const hasEmployeeRoleAndNoAdmin =
!invite.roles.includes('admin') && invite.roles.includes('employee');
!invite.roles.includes('admin') &&
(invite.roles.includes('employee') || invite.roles.includes('contractor'));
try {
if (hasEmployeeRoleAndNoAdmin) {
await addEmployeeWithoutInvite({
Expand Down Expand Up @@ -320,7 +321,8 @@ export function InviteMembersModal({

// Attempt to invite
const hasEmployeeRoleAndNoAdmin =
validRoles.includes('employee') && !validRoles.includes('admin');
(validRoles.includes('employee') || validRoles.includes('contractor')) &&
!validRoles.includes('admin');
try {
if (hasEmployeeRoleAndNoAdmin) {
await addEmployeeWithoutInvite({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export function MemberRow({ member, onRemove, onUpdateRole, canEdit }: MemberRow
return 'Auditor';
case 'employee':
return 'Employee';
case 'contractor':
return 'Contractor';
default:
return '???';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const selectableRoles: {
labelKey: 'people.roles.employee',
descriptionKey: 'people.roles.employee_description',
},
{
value: 'contractor',
labelKey: 'people.roles.contractor',
descriptionKey: 'people.roles.contractor_description',
},
{
value: 'auditor',
labelKey: 'people.roles.auditor',
Expand Down Expand Up @@ -95,6 +100,8 @@ export function MultiRoleCombobox({
return 'Auditor';
case 'employee':
return 'Employee';
case 'contractor':
return 'Contractor';
default:
return roleValue;
}
Expand All @@ -112,6 +119,8 @@ export function MultiRoleCombobox({
return 'Auditor';
case 'employee':
return 'Employee';
case 'contractor':
return 'Contractor';
case 'owner':
return 'Owner';
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export function MultiRoleComboboxContent({
return 'Auditor';
case 'employee':
return 'Employee';
case 'contractor':
return 'Contractor';
default:
return roleValue;
}
Expand All @@ -57,6 +59,8 @@ export function MultiRoleComboboxContent({
return 'Read-only access for compliance checks.';
case 'employee':
return 'Can sign policies and complete training.';
case 'contractor':
return 'Can sign policies and complete training.';
default:
return '';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,16 @@ export async function EmployeesOverview() {

employees = fetchedMembers.filter((member) => {
const roles = member.role.includes(',') ? member.role.split(',') : [member.role];
return roles.includes('employee');
return roles.includes('employee') || roles.includes('contractor');
});

// Fetch required policies
// Fetch required policies that are published and not archived
policies = await db.policy.findMany({
where: {
organizationId: organizationId,
isRequiredToSign: true,
status: 'published',
isArchived: false,
},
});

Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/app/(app)/[orgId]/people/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default async function Layout({ children }: { children: React.ReactNode }

const employees = allMembers.filter((member) => {
const roles = member.role.includes(',') ? member.role.split(',') : [member.role];
return roles.includes('employee');
return roles.includes('employee') || roles.includes('contractor');
});

return (
Expand Down
10 changes: 9 additions & 1 deletion apps/app/src/utils/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ export const employee = ac.newRole({
portal: ['read', 'update'],
});

export const allRoles = { owner, admin, auditor, employee } as const;
/**
* Contractor role with same permissions as employee
* Can manage portal for compliance purposes
*/
export const contractor = ac.newRole({
portal: ['read', 'update'],
});

export const allRoles = { owner, admin, auditor, employee, contractor } as const;
3 changes: 2 additions & 1 deletion apps/portal/src/app/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { nextCookies } from 'better-auth/next-js';
import { emailOTP, multiSession, organization } from 'better-auth/plugins';
import { ac, admin, auditor, employee, owner } from './permissions';
import { ac, admin, auditor, contractor, employee, owner } from './permissions';

export const auth = betterAuth({
database: prismaAdapter(db, {
Expand Down Expand Up @@ -46,6 +46,7 @@ export const auth = betterAuth({
admin,
auditor,
employee,
contractor,
},
schema: {
organization: {
Expand Down
8 changes: 8 additions & 0 deletions apps/portal/src/app/lib/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ export const employee = ac.newRole({
invitation: ['create', 'cancel'],
app: ['read', 'update'],
});

export const contractor = ac.newRole({
portal: ['read', 'update'],
organization: ['read', 'update'],
member: ['create', 'update'],
invitation: ['create', 'cancel'],
app: ['read', 'update'],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "public"."Role" ADD VALUE 'contractor';
1 change: 1 addition & 0 deletions packages/db/prisma/schema/auth.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ enum Role {
admin
auditor
employee
contractor
}

enum PolicyStatus {
Expand Down
Loading