Skip to content

Commit 86843f8

Browse files
github-actions[bot]chasprowebdevMarfuen
authored
[FEAT] Upload images for failed device policy - BUGFIX (#2039)
* fix(app): show preview menu on Employee Device UI * fix(app): remove unused imports in EmployeeTasks * fix(portal): pass organizationId to confirm-fleet-policy and fleet-policy endpoints * fix(app): pass FleetPolicyResults data to mark it passed on Employee Details UI * fix(app): pass organizationId to get-image-url endpoint * fix(portal): remove duplicated organizationId null check * fix(portal): add verification that user is a member of the org for endpoints * fix(app): add verification that user is a member of the org for get-image-url endpoint * fix(portal): update color of Pass text --------- Co-authored-by: chasprowebdev <chasgarciaprowebdev@gmail.com> Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
1 parent 31bc3a7 commit 86843f8

9 files changed

Lines changed: 92 additions & 39 deletions

File tree

apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import type { TrainingVideo } from '@/lib/data/training-videos';
44
import type { EmployeeTrainingVideoCompletion, Member, Organization, Policy, User } from '@db';
55

6-
import { cn } from '@/lib/utils';
76
import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card';
87
import {
98
Section,
@@ -14,9 +13,11 @@ import {
1413
TabsTrigger,
1514
Text,
1615
} from '@trycompai/design-system';
17-
import { AlertCircle, Award, CheckCircle2, Download, XCircle } from 'lucide-react';
16+
import { AlertCircle, Award, CheckCircle2, Download } from 'lucide-react';
1817
import type { FleetPolicy, Host } from '../../devices/types';
18+
import { PolicyItem } from '../../devices/components/PolicyItem';
1919
import { downloadTrainingCertificate } from '../actions/download-training-certificate';
20+
import { cn } from '@/lib/utils';
2021

2122
export const EmployeeTasks = ({
2223
employee,
@@ -220,28 +221,7 @@ export const EmployeeTasks = ({
220221
<CardTitle>{host.computer_name}&apos;s Policies</CardTitle>
221222
</CardHeader>
222223
<CardContent className="space-y-3">
223-
{fleetPolicies.map((policy) => (
224-
<div
225-
key={policy.id}
226-
className={cn(
227-
'hover:bg-muted/50 flex items-center justify-between rounded-md border border-l-4 p-3 shadow-sm transition-colors',
228-
policy.response === 'pass' ? 'border-l-primary' : 'border-l-destructive',
229-
)}
230-
>
231-
<Text weight="medium">{policy.name}</Text>
232-
{policy.response === 'pass' ? (
233-
<div className="flex items-center gap-1 text-primary">
234-
<CheckCircle2 size={16} />
235-
<Text size="sm">Pass</Text>
236-
</div>
237-
) : (
238-
<div className="flex items-center gap-1 text-destructive">
239-
<XCircle size={16} />
240-
<Text size="sm">Fail</Text>
241-
</div>
242-
)}
243-
</div>
244-
))}
224+
{fleetPolicies.map((policy) => <PolicyItem key={policy.id} policy={policy} />)}
245225
</CardContent>
246226
</Card>
247227
) : (

apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { headers } from 'next/headers';
1313
import { notFound, redirect } from 'next/navigation';
1414
import { Employee } from './components/Employee';
1515

16+
const MDM_POLICY_ID = -9999;
17+
1618
export default async function EmployeeDetailsPage({
1719
params,
1820
}: {
@@ -209,9 +211,38 @@ const getFleetPolicies = async (member: Member & { user: User }) => {
209211
}
210212

211213
const deviceWithPolicies = await fleet.get(`/hosts/${device.id}`);
212-
const fleetPolicies = deviceWithPolicies.data.host.policies || [];
213-
214-
return { fleetPolicies, device: deviceWithPolicies.data.host };
214+
const host = deviceWithPolicies.data.host;
215+
216+
const results = await db.fleetPolicyResult.findMany({
217+
where: {
218+
organizationId: member.organizationId,
219+
userId: member.userId,
220+
},
221+
orderBy: { createdAt: 'desc' },
222+
});
223+
224+
const platform = host.platform?.toLowerCase();
225+
const osVersion = host.os_version?.toLowerCase();
226+
const isMacOS =
227+
platform === 'darwin' ||
228+
platform === 'macos' ||
229+
platform === 'osx' ||
230+
osVersion?.includes('mac');
231+
232+
return {
233+
fleetPolicies: [
234+
...(host.policies || []),
235+
...(isMacOS ? [{ id: MDM_POLICY_ID, name: 'MDM Enabled', response: host.mdm.connected_to_fleet ? 'pass' : 'fail' }] : []),
236+
].map((policy) => {
237+
const policyResult = results.find((result) => result.fleetPolicyId === policy.id);
238+
return {
239+
...policy,
240+
response: policy.response === 'pass' || policyResult?.fleetPolicyResponse === 'pass' ? 'pass' : 'fail',
241+
attachments: policyResult?.attachments || [],
242+
};
243+
}),
244+
device: host
245+
};
215246
} catch (error) {
216247
console.error(
217248
`Failed to get device using individual fleet label for member: ${member.id}`,

apps/app/src/app/(app)/[orgId]/people/devices/components/PolicyImagePreview.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import useSWR from 'swr';
22
import Image from 'next/image';
3+
import { useParams } from 'next/navigation';
34

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

1617
export function PolicyImagePreview({ image }: { image: string }) {
18+
const params = useParams<{ orgId: string }>();
19+
const orgIdParam = params?.orgId;
20+
const organizationId = Array.isArray(orgIdParam) ? orgIdParam[0] : orgIdParam;
21+
1722
const { data: signedUrl, error, isLoading } = useSWR(
18-
() => (image ? `/api/get-image-url?key=${encodeURIComponent(image)}` : null),
23+
() =>
24+
image && organizationId
25+
? `/api/get-image-url?key=${encodeURIComponent(image)}&organizationId=${encodeURIComponent(organizationId)}`
26+
: null,
1927
fetcher,
2028
);
2129

apps/app/src/app/api/get-image-url/route.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { auth } from '@/utils/auth';
22
import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/app/s3';
33
import { GetObjectCommand } from '@aws-sdk/client-s3';
44
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5+
import { db } from '@db';
56
import { NextRequest, NextResponse } from 'next/server';
67

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

14-
const organizationId = session.session?.activeOrganizationId;
15+
const organizationId = req.nextUrl.searchParams.get('organizationId');
1516
if (!organizationId) {
1617
return NextResponse.json({ error: 'No active organization' }, { status: 400 });
1718
}
1819

20+
const member = await db.member.findFirst({
21+
where: {
22+
userId: session.user.id,
23+
organizationId,
24+
deactivated: false,
25+
},
26+
select: { id: true },
27+
});
28+
29+
if (!member) {
30+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
31+
}
32+
1933
const key = req.nextUrl.searchParams.get('key');
2034

2135
if (!key) {

apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/FleetPolicyItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function FleetPolicyItem({ policy }: FleetPolicyItemProps) {
8989
</div>
9090
<div className="flex items-center gap-3">
9191
{policy.response === 'pass' ? (
92-
<div className="flex items-center gap-1 text-green-600 dark:text-green-400">
92+
<div className="flex items-center gap-1 text-primary">
9393
<CheckCircle2 size={16} />
9494
<span className="text-sm">Pass</span>
9595
</div>

apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PolicyImageUploadModal.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@comp/ui/dialog';
1313
import { ImagePlus, Trash2, Loader2 } from 'lucide-react';
1414
import Image from 'next/image';
15-
import { useRouter } from 'next/navigation';
15+
import { useParams, useRouter } from 'next/navigation';
1616
import { toast } from 'sonner';
1717
import { FleetPolicy } from '../../types';
1818

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

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

5962
const handleSubmit = async () => {
6063
if (files.length === 0 || isLoading) return;
64+
if (!organizationId) {
65+
toast.error('Missing organization ID from URL');
66+
return;
67+
}
6168

6269
try {
6370
setIsLoading(true);
6471

6572
const formData = new FormData();
6673
formData.append('policyId', String(policy.id));
6774
formData.append('policyName', policy.name);
75+
formData.append('organizationId', organizationId);
6876

6977
files.forEach(({ file }) => {
7078
formData.append('files', file, file.name);

apps/portal/src/app/(app)/(home)/[orgId]/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const getFleetPolicies = async (
107107
];
108108

109109
// Get Policy Results from the database.
110-
const fleetPolicyResults = await getFleetPolicyResults();
110+
const fleetPolicyResults = await getFleetPolicyResults(member.organizationId);
111111
return {
112112
device,
113113
fleetPolicies: fleetPolicies.map((policy) => {
@@ -130,10 +130,10 @@ const getFleetPolicies = async (
130130
}
131131
};
132132

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

138138
const res = await fetch(url, {
139139
method: 'GET',

apps/portal/src/app/api/confirm-fleet-policy/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { auth } from '@/app/lib/auth';
22
import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/utils/s3';
3+
import { validateMemberAndOrg } from '@/app/api/download-agent/utils';
34
import { DeleteObjectsCommand, PutObjectCommand } from '@aws-sdk/client-s3';
45
import { db } from '@db';
56
import { Buffer } from 'node:buffer';
@@ -23,16 +24,21 @@ export async function POST(req: NextRequest) {
2324
const formData = await req.formData();
2425
const policyIdValue = formData.get('policyId');
2526
const policyName = formData.get('policyName');
27+
const organizationId = formData.get('organizationId') as string;
2628
const files = formData.getAll('files');
2729

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

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

37+
const member = await validateMemberAndOrg(userId, organizationId);
38+
if (!member) {
39+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
40+
}
41+
3642
if (!policyId || Number.isNaN(policyId)) {
3743
return NextResponse.json({ error: 'Invalid policyId' }, { status: 400 });
3844
}

apps/portal/src/app/api/fleet-policy/route.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { auth } from '@/app/lib/auth';
2+
import { validateMemberAndOrg } from '@/app/api/download-agent/utils';
23
import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/utils/s3';
34
import { GetObjectCommand } from '@aws-sdk/client-s3';
45
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
@@ -9,16 +10,21 @@ export const runtime = 'nodejs';
910
export const dynamic = 'force-dynamic';
1011

1112
export async function GET(req: NextRequest) {
13+
const organizationId = req.nextUrl.searchParams.get('organizationId');
14+
15+
if (!organizationId) {
16+
return NextResponse.json({ error: 'No organization ID' }, { status: 400 });
17+
}
18+
1219
const session = await auth.api.getSession({ headers: req.headers });
1320

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

18-
const organizationId = session.session?.activeOrganizationId;
19-
20-
if (!organizationId) {
21-
return NextResponse.json({ error: 'No active organization' }, { status: 400 });
25+
const member = await validateMemberAndOrg(session.user.id, organizationId);
26+
if (!member) {
27+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
2228
}
2329

2430
const results = await db.fleetPolicyResult.findMany({

0 commit comments

Comments
 (0)