Skip to content

Commit 7391136

Browse files
committed
chore: merge main into release for new releases
2 parents e77d78c + 3a4e9ee commit 7391136

File tree

161 files changed

+8568
-473
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

161 files changed

+8568
-473
lines changed

.husky/commit-msg

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
#!/usr/bin/env sh
2-
. "$(dirname "$0")/_/husky.sh"
3-
41
npx commitlint --edit $1

SELF_HOSTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ App (`apps/app`):
4646
- **APP_AWS_REGION**, **APP_AWS_ACCESS_KEY_ID**, **APP_AWS_SECRET_ACCESS_KEY**, **APP_AWS_BUCKET_NAME**: AWS S3 credentials for file storage (attachments, general uploads).
4747
- **APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET**: AWS S3 bucket name specifically for questionnaire file uploads. Required for the Security Questionnaire feature. If not set, users will see an error when trying to parse questionnaires.
4848
- **APP_AWS_KNOWLEDGE_BASE_BUCKET**: AWS S3 bucket name specifically for knowledge base documents. Required for the Knowledge Base feature in Security Questionnaire. If not set, users will see an error when trying to upload knowledge base documents.
49+
- **APP_AWS_ORG_ASSETS_BUCKET**: AWS S3 bucket name for organization static assets (e.g., company logos). Required for logo uploads in organization settings. If not set, logo upload will fail.
4950
- **OPENAI_API_KEY**: Enables AI features that call OpenAI models.
5051
- **UPSTASH_REDIS_REST_URL**, **UPSTASH_REDIS_REST_TOKEN**: Optional Redis (Upstash) used for rate limiting/queues/caching.
5152
- **NEXT_PUBLIC_POSTHOG_KEY**, **NEXT_PUBLIC_POSTHOG_HOST**: Client analytics via PostHog; leave unset to disable.
@@ -153,6 +154,7 @@ NEXT_PUBLIC_BETTER_AUTH_URL_PORTAL=http://localhost:3002
153154
# APP_AWS_BUCKET_NAME=
154155
# APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET=
155156
# APP_AWS_KNOWLEDGE_BASE_BUCKET=
157+
# APP_AWS_ORG_ASSETS_BUCKET=
156158
# OPENAI_API_KEY=
157159
# UPSTASH_REDIS_REST_URL=
158160
# UPSTASH_REDIS_REST_TOKEN=
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use server';
2+
3+
import { authActionClient } from '@/actions/safe-action';
4+
import { APP_AWS_ORG_ASSETS_BUCKET, s3Client } from '@/app/s3';
5+
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
6+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
7+
import { db } from '@db';
8+
import { revalidatePath } from 'next/cache';
9+
import { z } from 'zod';
10+
11+
const updateLogoSchema = z.object({
12+
fileName: z.string(),
13+
fileType: z.string(),
14+
fileData: z.string(), // base64 encoded
15+
});
16+
17+
export const updateOrganizationLogoAction = authActionClient
18+
.inputSchema(updateLogoSchema)
19+
.metadata({
20+
name: 'update-organization-logo',
21+
track: {
22+
event: 'update-organization-logo',
23+
channel: 'server',
24+
},
25+
})
26+
.action(async ({ parsedInput, ctx }) => {
27+
const { fileName, fileType, fileData } = parsedInput;
28+
const organizationId = ctx.session.activeOrganizationId;
29+
30+
if (!organizationId) {
31+
throw new Error('No active organization');
32+
}
33+
34+
// Validate file type
35+
if (!fileType.startsWith('image/')) {
36+
throw new Error('Only image files are allowed');
37+
}
38+
39+
// Check S3 client
40+
if (!s3Client || !APP_AWS_ORG_ASSETS_BUCKET) {
41+
throw new Error('File upload service is not available');
42+
}
43+
44+
// Convert base64 to buffer
45+
const fileBuffer = Buffer.from(fileData, 'base64');
46+
47+
// Validate file size (2MB limit for logos)
48+
const MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024;
49+
if (fileBuffer.length > MAX_FILE_SIZE_BYTES) {
50+
throw new Error('Logo must be less than 2MB');
51+
}
52+
53+
// Generate S3 key
54+
const timestamp = Date.now();
55+
const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
56+
const key = `${organizationId}/logo/${timestamp}-${sanitizedFileName}`;
57+
58+
// Upload to S3
59+
const putCommand = new PutObjectCommand({
60+
Bucket: APP_AWS_ORG_ASSETS_BUCKET,
61+
Key: key,
62+
Body: fileBuffer,
63+
ContentType: fileType,
64+
});
65+
await s3Client.send(putCommand);
66+
67+
// Update organization with new logo key
68+
await db.organization.update({
69+
where: { id: organizationId },
70+
data: { logo: key },
71+
});
72+
73+
// Generate signed URL for immediate display
74+
const getCommand = new GetObjectCommand({
75+
Bucket: APP_AWS_ORG_ASSETS_BUCKET,
76+
Key: key,
77+
});
78+
const signedUrl = await getSignedUrl(s3Client, getCommand, {
79+
expiresIn: 3600,
80+
});
81+
82+
revalidatePath(`/${organizationId}/settings`);
83+
84+
return { success: true, logoUrl: signedUrl };
85+
});
86+
87+
export const removeOrganizationLogoAction = authActionClient
88+
.inputSchema(z.object({}))
89+
.metadata({
90+
name: 'remove-organization-logo',
91+
track: {
92+
event: 'remove-organization-logo',
93+
channel: 'server',
94+
},
95+
})
96+
.action(async ({ ctx }) => {
97+
const organizationId = ctx.session.activeOrganizationId;
98+
99+
if (!organizationId) {
100+
throw new Error('No active organization');
101+
}
102+
103+
// Remove logo from organization
104+
await db.organization.update({
105+
where: { id: organizationId },
106+
data: { logo: null },
107+
});
108+
109+
revalidatePath(`/${organizationId}/settings`);
110+
111+
return { success: true };
112+
});
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
'use client';
2+
3+
import { Download } from 'lucide-react';
4+
import Image from 'next/image';
5+
6+
interface AuditorViewProps {
7+
initialContent: Record<string, string>;
8+
organizationName: string;
9+
logoUrl: string | null;
10+
employeeCount: string | null;
11+
cSuite: { name: string; title: string }[];
12+
reportSignatory: { fullName: string; jobTitle: string; email: string } | null;
13+
}
14+
15+
export function AuditorView({
16+
initialContent,
17+
organizationName,
18+
logoUrl,
19+
employeeCount,
20+
cSuite,
21+
reportSignatory,
22+
}: AuditorViewProps) {
23+
return (
24+
<div className="flex flex-col gap-10">
25+
{/* Header */}
26+
<div className="flex items-center gap-4">
27+
{logoUrl && (
28+
<a
29+
href={logoUrl}
30+
download={`${organizationName.replace(/[^a-zA-Z0-9]/g, '_')}_logo`}
31+
className="group relative h-14 w-14 shrink-0 overflow-hidden rounded-lg border bg-background transition-all hover:shadow-md"
32+
title="Download logo"
33+
>
34+
<Image src={logoUrl} alt={`${organizationName} logo`} fill className="object-contain" />
35+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
36+
<Download className="h-4 w-4 text-white" />
37+
</div>
38+
</a>
39+
)}
40+
<div>
41+
<h1 className="text-foreground text-xl font-semibold tracking-tight">
42+
{organizationName}
43+
</h1>
44+
<p className="text-muted-foreground text-sm">Company Overview</p>
45+
</div>
46+
</div>
47+
48+
{/* Company Information */}
49+
<Section title="Company Information">
50+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
51+
<InfoCell
52+
label="Employees"
53+
value={employeeCount || '—'}
54+
className="lg:border-r lg:border-border lg:pr-6"
55+
/>
56+
<InfoCell
57+
label="Report Signatory"
58+
className="lg:border-r lg:border-border lg:pr-6"
59+
value={
60+
reportSignatory ? (
61+
<div>
62+
<div className="flex items-baseline gap-2">
63+
<span className="font-medium">{reportSignatory.fullName}</span>
64+
<span className="text-muted-foreground text-xs">
65+
{reportSignatory.jobTitle}
66+
</span>
67+
</div>
68+
<div className="text-muted-foreground text-xs mt-0.5">
69+
{reportSignatory.email}
70+
</div>
71+
</div>
72+
) : (
73+
'—'
74+
)
75+
}
76+
/>
77+
<InfoCell
78+
label="Executive Team"
79+
className="sm:col-span-2 lg:col-span-1"
80+
value={
81+
cSuite.length > 0 ? (
82+
<div className="space-y-1">
83+
{cSuite.map((exec, i) => (
84+
<div key={i} className="flex items-baseline gap-2 text-sm">
85+
<span className="font-medium">{exec.name}</span>
86+
<span className="text-muted-foreground text-xs">{exec.title}</span>
87+
</div>
88+
))}
89+
</div>
90+
) : (
91+
'—'
92+
)
93+
}
94+
/>
95+
</div>
96+
</Section>
97+
98+
{/* Business Overview */}
99+
<Section title="Business Overview">
100+
<div className="space-y-6">
101+
<ContentRow
102+
title="Company Background & Overview of Operations"
103+
content={initialContent['Company Background & Overview of Operations']}
104+
/>
105+
<ContentRow
106+
title="Types of Services Provided"
107+
content={initialContent['Types of Services Provided']}
108+
/>
109+
<ContentRow title="Mission & Vision" content={initialContent['Mission & Vision']} />
110+
</div>
111+
</Section>
112+
113+
{/* System Architecture */}
114+
<Section title="System Architecture">
115+
<ContentRow title="System Description" content={initialContent['System Description']} />
116+
</Section>
117+
118+
{/* Third Party Dependencies */}
119+
<Section title="Third Party Dependencies">
120+
<div className="grid gap-6 lg:grid-cols-2">
121+
<ContentRow title="Critical Vendors" content={initialContent['Critical Vendors']} />
122+
<ContentRow
123+
title="Subservice Organizations"
124+
content={initialContent['Subservice Organizations']}
125+
/>
126+
</div>
127+
</Section>
128+
</div>
129+
);
130+
}
131+
132+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
133+
return (
134+
<div className="space-y-4">
135+
<div className="flex items-center gap-3 border-b border-border pb-2">
136+
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
137+
{title}
138+
</h2>
139+
</div>
140+
{children}
141+
</div>
142+
);
143+
}
144+
145+
function InfoCell({
146+
label,
147+
value,
148+
className,
149+
}: {
150+
label: string;
151+
value: React.ReactNode;
152+
className?: string;
153+
}) {
154+
return (
155+
<div className={className || ''}>
156+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground mb-1.5">
157+
{label}
158+
</div>
159+
<div className="text-sm text-foreground">{value}</div>
160+
</div>
161+
);
162+
}
163+
164+
function ContentRow({ title, content }: { title: string; content?: string }) {
165+
const hasContent = content?.trim().length;
166+
167+
return (
168+
<div className="space-y-1.5">
169+
<h3 className="text-sm font-medium text-foreground">{title}</h3>
170+
{hasContent ? (
171+
<p className="text-sm leading-relaxed text-muted-foreground whitespace-pre-wrap">
172+
{content}
173+
</p>
174+
) : (
175+
<p className="text-xs text-muted-foreground/50">Not yet available</p>
176+
)}
177+
</div>
178+
);
179+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default function Layout({ children }: { children: React.ReactNode }) {
2+
return <div className="m-auto max-w-[1200px] py-8">{children}</div>;
3+
}
4+

0 commit comments

Comments
 (0)