Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ff3f6f7
refactor: enhance EmployeeCompletionChart with profile links
Marfuen Aug 26, 2025
50222fa
Merge pull request #1411 from trycompai/mariano/new-agent
Marfuen Aug 26, 2025
8a81f42
fix: Enforce role-based access control in app
Dhanus3133 Aug 26, 2025
a9957cd
fix: Prisma seed command in `packages/db`
Dhanus3133 Aug 26, 2025
d7ac38b
Merge pull request #1414 from Dhanus3133/fix/prisma-seed-command
Marfuen Aug 27, 2025
f8700e9
Merge branch 'main' into fix/deny-access-non-owners-and-non-admin
Dhanus3133 Aug 27, 2025
a4056bf
fix: Move role checks on org level
Dhanus3133 Aug 27, 2025
237a1fc
Merge branch 'fix/deny-access-non-owners-and-non-admin' of https://gi…
Dhanus3133 Aug 27, 2025
1553400
fix: Allow access to auditor role
Dhanus3133 Aug 27, 2025
bfa18f0
chore: Just restrict access to employee role
Dhanus3133 Aug 27, 2025
4bedd51
Merge pull request #1413 from Dhanus3133/fix/deny-access-non-owners-a…
Marfuen Aug 27, 2025
f16ad2f
chore(deps): bump @tiptap/extension-highlight from 2.22.3 to 3.3.0 (#…
dependabot[bot] Aug 28, 2025
ee901ab
chore(deps): bump dub from 0.63.7 to 0.66.1 (#1399)
dependabot[bot] Aug 28, 2025
0740452
chore(deps): bump @dub/embed-react from 0.0.15 to 0.0.16 (#1337)
dependabot[bot] Aug 28, 2025
dd928ad
chore: update Header component and enhance NoAccess page layout (#1425)
github-actions[bot] Aug 28, 2025
9035238
fix: remove duplicate dependsOn key (#1426)
golamrabbiazad Aug 28, 2025
abedc9a
[dev] [Marfuen] mariano/videos (#1427)
github-actions[bot] Aug 28, 2025
1a3b08d
[dev] [Marfuen] mariano/fix-bug (#1429)
github-actions[bot] Aug 28, 2025
a717403
Mariano/updated employees (#1430)
Marfuen Aug 28, 2025
98c6503
chore: remove training video backfill action and related documentatio…
github-actions[bot] Aug 28, 2025
2691ce8
[dev] [Marfuen] mariano/batch (#1432)
github-actions[bot] Aug 28, 2025
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
4 changes: 2 additions & 2 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@dub/analytics": "^0.0.27",
"@dub/better-auth": "^0.0.3",
"@dub/embed-react": "^0.0.15",
"@dub/embed-react": "^0.0.16",
"@hookform/resolvers": "^5.1.1",
"@mendable/firecrawl-js": "^1.24.0",
"@nangohq/frontend": "^0.53.2",
Expand Down Expand Up @@ -57,7 +57,7 @@
"better-auth": "^1.2.8",
"canvas-confetti": "^1.9.3",
"d3": "^7.9.0",
"dub": "^0.63.6",
"dub": "^0.66.1",
"framer-motion": "^12.18.1",
"geist": "^1.3.1",
"lucide-react": "^0.534.0",
Expand Down
58 changes: 58 additions & 0 deletions apps/app/scripts/backfill-training-videos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env tsx

/**
* Script to trigger the training video completion backfill job.
*
* Usage:
* # Backfill all organizations
* bun run scripts/backfill-training-videos.ts
*
* # Backfill specific organization
* bun run scripts/backfill-training-videos.ts --org <organizationId>
*
* This script is useful for:
* - Running the backfill manually
* - Testing the backfill process
* - Running on-demand backfills for specific organizations
*/

import { backfillTrainingVideosForAllOrgs } from '@/jobs/tasks/onboarding/backfill-training-videos-for-all-orgs';
import { backfillTrainingVideosForOrg } from '@/jobs/tasks/onboarding/backfill-training-videos-for-org';

async function main() {
const args = process.argv.slice(2);
const orgIndex = args.indexOf('--org');
const organizationId = orgIndex !== -1 ? args[orgIndex + 1] : null;

try {
if (organizationId) {
console.log(`🚀 Triggering training video backfill for organization: ${organizationId}`);

const handle = await backfillTrainingVideosForOrg.trigger({
organizationId: organizationId,
});

console.log(`✅ Successfully triggered job with ID: ${handle.id}`);
console.log(`📊 You can monitor the progress in the Trigger.dev dashboard`);
} else {
console.log('🚀 Triggering training video backfill for ALL organizations');

const handle = await backfillTrainingVideosForAllOrgs.trigger();

console.log(`✅ Successfully triggered batch job with ID: ${handle.id}`);
console.log(`📊 You can monitor the progress in the Trigger.dev dashboard`);
console.log(`⚠️ This will process ALL organizations and their members`);
}
} catch (error) {
console.error('❌ Error triggering backfill job:', error);
process.exit(1);
}
}

// Only run if this script is executed directly
if (require.main === module) {
main().catch((error) => {
console.error('❌ Script failed:', error);
process.exit(1);
});
}
6 changes: 5 additions & 1 deletion apps/app/src/actions/organization/accept-invitation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use server';

import { createTrainingVideoEntries } from '@/lib/db/employee';
import { db } from '@db';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
Expand Down Expand Up @@ -96,7 +97,7 @@ export const completeInvitation = authActionClientWithoutOrg
throw new Error('Invitation role is required');
}

await db.member.create({
const newMember = await db.member.create({
data: {
userId: user.id,
organizationId: invitation.organizationId,
Expand All @@ -105,6 +106,9 @@ export const completeInvitation = authActionClientWithoutOrg
},
});

// Create training video completion entries for the new member
await createTrainingVideoEntries(newMember.id);

await db.invitation.update({
where: {
id: invitation.id,
Expand Down
4 changes: 4 additions & 0 deletions apps/app/src/app/(app)/[orgId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export default async function Layout({
return redirect('/auth/unauthorized');
}

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

// If this org is not accessible on current plan, redirect to upgrade
if (!organization.hasAccess) {
return redirect(`/upgrade/${organization.id}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use server';

import { createTrainingVideoEntries } from '@/lib/db/employee';
import { auth } from '@/utils/auth';
import type { Role } from '@db';
import { db } from '@db';
Expand Down Expand Up @@ -61,6 +62,11 @@ export const addEmployeeWithoutInvite = async ({
},
});

// Create training video completion entries for the new member
if (member?.id) {
await createTrainingVideoEntries(member.id);
}

return { success: true, data: member };
} catch (error) {
console.error('Error adding employee:', error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card';
import { Input } from '@comp/ui/input';
import { Search } from 'lucide-react';
import { ExternalLink, Search } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import type { CSSProperties } from 'react';
import * as React from 'react';

Expand Down Expand Up @@ -47,6 +49,8 @@ export function EmployeeCompletionChart({
trainingVideos,
showAll = false,
}: EmployeeCompletionChartProps) {
const params = useParams();
const orgId = params.orgId as string;
const [searchTerm, setSearchTerm] = React.useState('');
const [displayedItems, setDisplayedItems] = React.useState(showAll ? 20 : 5);
const [isLoading, setIsLoading] = React.useState(false);
Expand Down Expand Up @@ -209,14 +213,30 @@ export function EmployeeCompletionChart({
{sortedStats.map((stat) => (
<div key={stat.id} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div>
<p className="font-medium">{stat.name}</p>
<div className="flex-1">
<div className="flex items-center gap-2">
<p className="font-medium">{stat.name}</p>
<Link
href={`/${orgId}/people/${stat.id}`}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/80 flex items-center gap-1 text-xs font-medium underline-offset-4 hover:underline"
>
View Profile
<ExternalLink className="h-3 w-3" />
</Link>
</div>
<p className="text-muted-foreground text-xs">{stat.email}</p>
</div>
<span className="text-muted-foreground">
{stat.policiesCompleted + stat.trainingsCompleted} / {stat.totalTasks}{' '}
{'tasks'}
</span>
<div className="text-muted-foreground text-right text-xs">
<div>
{stat.policiesCompleted + stat.trainingsCompleted} / {stat.totalTasks} tasks
</div>
<div className="text-xs">
{stat.policiesCompleted}/{stat.policiesTotal} policies •{' '}
{stat.trainingsCompleted}/{stat.trainingsTotal} training
</div>
</div>
</div>

<TaskBarChart stat={stat} />
Expand Down
32 changes: 18 additions & 14 deletions apps/app/src/app/(app)/no-access/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Header } from '@/components/header';
import { OrganizationSwitcher } from '@/components/organization-switcher';
import { auth } from '@/utils/auth';
import { db } from '@db';
Expand Down Expand Up @@ -31,20 +32,23 @@ export default async function NoAccess() {
});

return (
<div className="bg-foreground/05 flex h-dvh flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">Access Denied</h1>
<div className="flex flex-col text-center">
<p>
<b>Employees</b> don&apos;t have access to app.trycomp.ai, did you mean to go to{' '}
<Link href="https://portal.trycomp.ai" className="text-primary underline">
portal.trycomp.ai
</Link>
?
</p>
<p>Please select another organization or contact your organization administrator.</p>
</div>
<div>
<OrganizationSwitcher organizations={organizations} organization={currentOrg} />
<div className="flex h-dvh flex-col">
<Header organizationId={currentOrg?.id} hideChat={true} />
<div className="bg-foreground/05 flex flex-1 flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">Access Denied</h1>
<div className="flex flex-col text-center">
<p>
<b>Employees</b> don&apos;t have access to app.trycomp.ai, did you mean to go to{' '}
<Link href="https://portal.trycomp.ai" className="text-primary underline">
portal.trycomp.ai
</Link>
?
</p>
<p>Please select another organization or contact your organization administrator.</p>
</div>
<div>
<OrganizationSwitcher organizations={organizations} organization={currentOrg} />
</div>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { initializeOrganization } from '@/actions/organization/lib/initialize-organization';
import { authActionClientWithoutOrg } from '@/actions/safe-action';
import { createTrainingVideoEntries } from '@/lib/db/employee';
import { auth } from '@/utils/auth';
import { db } from '@db';
import { revalidatePath } from 'next/cache';
Expand Down Expand Up @@ -64,6 +65,19 @@ export const createOrganizationMinimal = authActionClientWithoutOrg

const orgId = newOrg.id;

// Get the member that was created with the organization (the owner)
const ownerMember = await db.member.findFirst({
where: {
userId: session.user.id,
organizationId: orgId,
},
});

// Create training video completion entries for the owner
if (ownerMember) {
await createTrainingVideoEntries(ownerMember.id);
}

// Create onboarding record for new org
await db.onboarding.create({
data: {
Expand Down
14 changes: 14 additions & 0 deletions apps/app/src/app/(app)/setup/actions/create-organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { initializeOrganization } from '@/actions/organization/lib/initialize-or
import { authActionClientWithoutOrg } from '@/actions/safe-action';
import { createFleetLabelForOrg } from '@/jobs/tasks/device/create-fleet-label-for-org';
import { onboardOrganization as onboardOrganizationTask } from '@/jobs/tasks/onboarding/onboard-organization';
import { createTrainingVideoEntries } from '@/lib/db/employee';
import { auth } from '@/utils/auth';
import { db } from '@db';
import { tasks } from '@trigger.dev/sdk';
Expand Down Expand Up @@ -63,6 +64,19 @@ export const createOrganization = authActionClientWithoutOrg

const orgId = newOrg.id;

// Get the member that was created with the organization (the owner)
const ownerMember = await db.member.findFirst({
where: {
userId: session.user.id,
organizationId: orgId,
},
});

// Create training video completion entries for the owner
if (ownerMember) {
await createTrainingVideoEntries(ownerMember.id);
}

// Create onboarding record for new org
await db.onboarding.create({
data: {
Expand Down
4 changes: 0 additions & 4 deletions apps/app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ export default async function RootPage({
},
});

if (member?.role === 'employee') {
return redirect(await buildUrlWithParams('/no-access'));
}

if (!member) {
return redirect(await buildUrlWithParams('/setup'));
}
Expand Down
10 changes: 8 additions & 2 deletions apps/app/src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import { Suspense } from 'react';
import { AssistantButton } from './ai/chat-button';
import { MobileMenu } from './mobile-menu';

export async function Header({ organizationId }: { organizationId?: string }) {
export async function Header({
organizationId,
hideChat = false,
}: {
organizationId?: string;
hideChat?: boolean;
}) {
const { organizations } = await getOrganizations();

return (
<header className="border/40 sticky top-0 z-10 flex items-center justify-between border-b px-4 py-2 backdrop-blur-sm bg-card">
<MobileMenu organizations={organizations} organizationId={organizationId} />

<AssistantButton />
{!hideChat && <AssistantButton />}

<div className="ml-auto flex space-x-2">
<Suspense fallback={<Skeleton className="h-8 w-8 rounded-full" />}>
Expand Down
Loading
Loading