Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import type { TrainingVideo } from '@/lib/data/training-videos';
import type { EmployeeTrainingVideoCompletion, Member, Organization, Policy, User } from '@db';

import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card';
import {
Section,
Expand All @@ -14,9 +13,11 @@ import {
TabsTrigger,
Text,
} from '@trycompai/design-system';
import { AlertCircle, Award, CheckCircle2, Download, XCircle } from 'lucide-react';
import { AlertCircle, Award, CheckCircle2, Download } from 'lucide-react';
import type { FleetPolicy, Host } from '../../devices/types';
import { PolicyItem } from '../../devices/components/PolicyItem';
import { downloadTrainingCertificate } from '../actions/download-training-certificate';
import { cn } from '@/lib/utils';

export const EmployeeTasks = ({
employee,
Expand Down Expand Up @@ -220,28 +221,7 @@ export const EmployeeTasks = ({
<CardTitle>{host.computer_name}&apos;s Policies</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{fleetPolicies.map((policy) => (
<div
key={policy.id}
className={cn(
'hover:bg-muted/50 flex items-center justify-between rounded-md border border-l-4 p-3 shadow-sm transition-colors',
policy.response === 'pass' ? 'border-l-primary' : 'border-l-destructive',
)}
>
<Text weight="medium">{policy.name}</Text>
{policy.response === 'pass' ? (
<div className="flex items-center gap-1 text-primary">
<CheckCircle2 size={16} />
<Text size="sm">Pass</Text>
</div>
) : (
<div className="flex items-center gap-1 text-destructive">
<XCircle size={16} />
<Text size="sm">Fail</Text>
</div>
)}
</div>
))}
{fleetPolicies.map((policy) => <PolicyItem key={policy.id} policy={policy} />)}
</CardContent>
</Card>
) : (
Expand Down
37 changes: 34 additions & 3 deletions apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { headers } from 'next/headers';
import { notFound, redirect } from 'next/navigation';
import { Employee } from './components/Employee';

const MDM_POLICY_ID = -9999;

export default async function EmployeeDetailsPage({
params,
}: {
Expand Down Expand Up @@ -209,9 +211,38 @@ const getFleetPolicies = async (member: Member & { user: User }) => {
}

const deviceWithPolicies = await fleet.get(`/hosts/${device.id}`);
const fleetPolicies = deviceWithPolicies.data.host.policies || [];

return { fleetPolicies, device: deviceWithPolicies.data.host };
const host = deviceWithPolicies.data.host;

const results = await db.fleetPolicyResult.findMany({
where: {
organizationId: member.organizationId,
userId: member.userId,
},
orderBy: { createdAt: 'desc' },
});

const platform = host.platform?.toLowerCase();
const osVersion = host.os_version?.toLowerCase();
const isMacOS =
platform === 'darwin' ||
platform === 'macos' ||
platform === 'osx' ||
osVersion?.includes('mac');

return {
fleetPolicies: [
...(host.policies || []),
...(isMacOS ? [{ id: MDM_POLICY_ID, name: 'MDM Enabled', response: host.mdm.connected_to_fleet ? 'pass' : 'fail' }] : []),
].map((policy) => {
const policyResult = results.find((result) => result.fleetPolicyId === policy.id);
return {
...policy,
response: policy.response === 'pass' || policyResult?.fleetPolicyResponse === 'pass' ? 'pass' : 'fail',
attachments: policyResult?.attachments || [],
};
}),
device: host
};
} catch (error) {
console.error(
`Failed to get device using individual fleet label for member: ${member.id}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import useSWR from 'swr';
import Image from 'next/image';
import { useParams } from 'next/navigation';

const fetcher = async (key: string) => {
const res = await fetch(key, { cache: 'no-store' });
Expand All @@ -14,8 +15,15 @@ const fetcher = async (key: string) => {
};

export function PolicyImagePreview({ image }: { image: string }) {
const params = useParams<{ orgId: string }>();
const orgIdParam = params?.orgId;
const organizationId = Array.isArray(orgIdParam) ? orgIdParam[0] : orgIdParam;

const { data: signedUrl, error, isLoading } = useSWR(
() => (image ? `/api/get-image-url?key=${encodeURIComponent(image)}` : null),
() =>
image && organizationId
? `/api/get-image-url?key=${encodeURIComponent(image)}&organizationId=${encodeURIComponent(organizationId)}`
: null,
fetcher,
);

Expand Down
16 changes: 15 additions & 1 deletion apps/app/src/app/api/get-image-url/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { auth } from '@/utils/auth';
import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/app/s3';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { db } from '@db';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
Expand All @@ -11,11 +12,24 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const organizationId = session.session?.activeOrganizationId;
const organizationId = req.nextUrl.searchParams.get('organizationId');
if (!organizationId) {
return NextResponse.json({ error: 'No active organization' }, { status: 400 });
}

const member = await db.member.findFirst({
where: {
userId: session.user.id,
organizationId,
deactivated: false,
},
select: { id: true },
});

if (!member) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

const key = req.nextUrl.searchParams.get('key');

if (!key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function FleetPolicyItem({ policy }: FleetPolicyItemProps) {
</div>
<div className="flex items-center gap-3">
{policy.response === 'pass' ? (
<div className="flex items-center gap-1 text-green-600 dark:text-green-400">
<div className="flex items-center gap-1 text-primary">
<CheckCircle2 size={16} />
<span className="text-sm">Pass</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@comp/ui/dialog';
import { ImagePlus, Trash2, Loader2 } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { FleetPolicy } from '../../types';

Expand All @@ -27,6 +27,9 @@ export function PolicyImageUploadModal({ open, onOpenChange, policy }: PolicyIma
const [files, setFiles] = useState<Array<{ file: File; previewUrl: string }>>([]);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const params = useParams<{ orgId: string }>();
const orgIdParam = params?.orgId;
const organizationId = Array.isArray(orgIdParam) ? orgIdParam[0] : orgIdParam;

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = Array.from(e.target.files ?? []);
Expand Down Expand Up @@ -58,13 +61,18 @@ export function PolicyImageUploadModal({ open, onOpenChange, policy }: PolicyIma

const handleSubmit = async () => {
if (files.length === 0 || isLoading) return;
if (!organizationId) {
toast.error('Missing organization ID from URL');
return;
}

try {
setIsLoading(true);

const formData = new FormData();
formData.append('policyId', String(policy.id));
formData.append('policyName', policy.name);
formData.append('organizationId', organizationId);

files.forEach(({ file }) => {
formData.append('files', file, file.name);
Expand Down
6 changes: 3 additions & 3 deletions apps/portal/src/app/(app)/(home)/[orgId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const getFleetPolicies = async (
];

// Get Policy Results from the database.
const fleetPolicyResults = await getFleetPolicyResults();
const fleetPolicyResults = await getFleetPolicyResults(member.organizationId);
return {
device,
fleetPolicies: fleetPolicies.map((policy) => {
Expand All @@ -130,10 +130,10 @@ const getFleetPolicies = async (
}
};

const getFleetPolicyResults = async (): Promise<FleetPolicyResult[]> => {
const getFleetPolicyResults = async (organizationId: string): Promise<FleetPolicyResult[]> => {
try {
const portalBase = process.env.NEXT_PUBLIC_BETTER_AUTH_URL?.replace(/\/$/, '');
const url = `${portalBase}/api/fleet-policy`;
const url = `${portalBase}/api/fleet-policy?organizationId=${organizationId}`;

const res = await fetch(url, {
method: 'GET',
Expand Down
8 changes: 7 additions & 1 deletion apps/portal/src/app/api/confirm-fleet-policy/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { auth } from '@/app/lib/auth';
import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/utils/s3';
import { validateMemberAndOrg } from '@/app/api/download-agent/utils';
import { DeleteObjectsCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { db } from '@db';
import { Buffer } from 'node:buffer';
Expand All @@ -23,16 +24,21 @@ export async function POST(req: NextRequest) {
const formData = await req.formData();
const policyIdValue = formData.get('policyId');
const policyName = formData.get('policyName');
const organizationId = formData.get('organizationId') as string;
const files = formData.getAll('files');

const policyId = typeof policyIdValue === 'string' ? Number(policyIdValue) : null;
const organizationId = session.session?.activeOrganizationId;
const userId = session.user.id;

if (!organizationId) {
return NextResponse.json({ error: 'No active organization' }, { status: 400 });
}

const member = await validateMemberAndOrg(userId, organizationId);
if (!member) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

if (!policyId || Number.isNaN(policyId)) {
return NextResponse.json({ error: 'Invalid policyId' }, { status: 400 });
}
Expand Down
14 changes: 10 additions & 4 deletions apps/portal/src/app/api/fleet-policy/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { auth } from '@/app/lib/auth';
import { validateMemberAndOrg } from '@/app/api/download-agent/utils';
import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/utils/s3';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
Expand All @@ -9,16 +10,21 @@ export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(req: NextRequest) {
const organizationId = req.nextUrl.searchParams.get('organizationId');

if (!organizationId) {
return NextResponse.json({ error: 'No organization ID' }, { status: 400 });
}

const session = await auth.api.getSession({ headers: req.headers });

if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const organizationId = session.session?.activeOrganizationId;

if (!organizationId) {
return NextResponse.json({ error: 'No active organization' }, { status: 400 });
const member = await validateMemberAndOrg(session.user.id, organizationId);
if (!member) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}

const results = await db.fleetPolicyResult.findMany({
Expand Down