diff --git a/CLAUDE.md b/CLAUDE.md index 523ff9ae7..fc57dda3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,6 +109,7 @@ Every customer-facing API endpoint MUST have: - **DS components that do NOT accept `className`**: `Text`, `Stack`, `HStack`, `Badge`, `Button` — wrap in `
` for custom styling - **Layout**: Use `PageLayout`, `PageHeader`, `Stack`, `HStack`, `Section`, `SettingGroup` - **Patterns**: Sheet (`Sheet > SheetContent > SheetHeader + SheetBody`), Drawer, Collapsible +- **After editing any frontend component**: Run the `audit-design-system` skill to catch `@comp/ui` or `lucide-react` imports that should be migrated ## Data Fetching diff --git a/apps/api/src/auth/auth.server.ts b/apps/api/src/auth/auth.server.ts index 1085f81bd..6f53b8ec3 100644 --- a/apps/api/src/auth/auth.server.ts +++ b/apps/api/src/auth/auth.server.ts @@ -272,8 +272,13 @@ export const auth = betterAuth({ magicLink({ expiresIn: MAGIC_LINK_EXPIRES_IN_SECONDS, sendMagicLink: async ({ email, url }) => { + // The `url` from better-auth points to the API's verify endpoint + // and includes the callbackURL from the client's sign-in request. + // Flow: user clicks link → API verifies token & sets session cookie + // → API redirects (302) to callbackURL (the app). if (process.env.NODE_ENV === 'development') { console.log('[Auth] Sending magic link to:', email); + console.log('[Auth] Magic link URL:', url); } await triggerEmail({ to: email, diff --git a/apps/api/src/email/trigger-email.ts b/apps/api/src/email/trigger-email.ts index f95b61f01..735c89182 100644 --- a/apps/api/src/email/trigger-email.ts +++ b/apps/api/src/email/trigger-email.ts @@ -14,34 +14,43 @@ export async function triggerEmail(params: { scheduledAt?: string; attachments?: EmailAttachment[]; }): Promise<{ id: string }> { - const html = await render(params.react); + try { + const html = await render(params.react); - const fromMarketing = process.env.RESEND_FROM_MARKETING; - const fromSystem = process.env.RESEND_FROM_SYSTEM; - const fromDefault = process.env.RESEND_FROM_DEFAULT; + const fromMarketing = process.env.RESEND_FROM_MARKETING; + const fromSystem = process.env.RESEND_FROM_SYSTEM; + const fromDefault = process.env.RESEND_FROM_DEFAULT; - const fromAddress = params.marketing - ? fromMarketing - : params.system - ? fromSystem - : fromDefault; + const fromAddress = params.marketing + ? fromMarketing + : params.system + ? fromSystem + : fromDefault; - const handle = await tasks.trigger('send-email', { - to: params.to, - subject: params.subject, - html, - from: fromAddress ?? undefined, - cc: params.cc, - scheduledAt: params.scheduledAt, - attachments: params.attachments?.map((att) => ({ - filename: att.filename, - content: - typeof att.content === 'string' - ? att.content - : att.content.toString('base64'), - contentType: att.contentType, - })), - }); + const handle = await tasks.trigger('send-email', { + to: params.to, + subject: params.subject, + html, + from: fromAddress ?? undefined, + cc: params.cc, + scheduledAt: params.scheduledAt, + attachments: params.attachments?.map((att) => ({ + filename: att.filename, + content: + typeof att.content === 'string' + ? att.content + : att.content.toString('base64'), + contentType: att.contentType, + })), + }); - return { id: handle.id }; + return { id: handle.id }; + } catch (error) { + console.error('[triggerEmail] Failed to trigger email task', { + to: params.to, + subject: params.subject, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } } diff --git a/apps/api/src/trigger/email/send-email.ts b/apps/api/src/trigger/email/send-email.ts index b37357208..36a99bdce 100644 --- a/apps/api/src/trigger/email/send-email.ts +++ b/apps/api/src/trigger/email/send-email.ts @@ -32,10 +32,13 @@ export const sendEmailTask = schemaTask({ }), run: async (params) => { if (!resend) { + logger.error('Resend not initialized - missing RESEND_API_KEY', { + to: params.to, + subject: params.subject, + }); throw new Error('Resend not initialized - missing API key'); } - const fromMarketing = process.env.RESEND_FROM_MARKETING; const fromSystem = process.env.RESEND_FROM_SYSTEM; const fromDefault = process.env.RESEND_FROM_DEFAULT; const toTest = process.env.RESEND_TO_TEST; @@ -47,27 +50,40 @@ export const sendEmailTask = schemaTask({ throw new Error('Missing FROM address in environment variables'); } - const { data, error } = await resend.emails.send({ - from: fromAddress, - to: toAddress, - cc: params.cc, - subject: params.subject, - html: params.html, - scheduledAt: params.scheduledAt, - attachments: params.attachments?.map((att) => ({ - filename: att.filename, - content: att.content, - contentType: att.contentType, - })), - }); + try { + const { data, error } = await resend.emails.send({ + from: fromAddress, + to: toAddress, + cc: params.cc, + subject: params.subject, + html: params.html, + scheduledAt: params.scheduledAt, + attachments: params.attachments?.map((att) => ({ + filename: att.filename, + content: att.content, + contentType: att.contentType, + })), + }); - if (error) { - logger.error('Resend API error', { error }); - throw new Error(`Failed to send email: ${error.message}`); - } + if (error) { + logger.error('Resend API error', { + error, + to: params.to, + subject: params.subject, + }); + throw new Error(`Failed to send email: ${error.message}`); + } - logger.info('Email sent', { to: params.to, id: data?.id }); + logger.info('Email sent', { to: params.to, id: data?.id }); - return { id: data?.id }; + return { id: data?.id }; + } catch (error) { + logger.error('Email sending failed', { + to: params.to, + subject: params.subject, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } }, }); diff --git a/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx b/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx index 73a91a2ca..a3198136a 100644 --- a/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/AppSidebar.tsx @@ -103,12 +103,6 @@ export function AppSidebar({ name: 'Cloud Tests', hidden: !canAccessRoute(permissions, 'cloud-tests'), }, - { - id: 'penetration-tests', - path: `/${organization.id}/security/penetration-tests`, - name: 'Penetration Tests', - hidden: !canAccessRoute(permissions, 'penetration-tests'), - }, ]; const isPathActive = (itemPath: string) => { diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/AutomationRunsCard.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/AutomationRunsCard.tsx index 9fc95fdd1..61d3a9c08 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/AutomationRunsCard.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/AutomationRunsCard.tsx @@ -4,8 +4,9 @@ import { Badge } from '@comp/ui/badge'; import { EvidenceAutomationRun, EvidenceAutomationRunStatus } from '@db'; import { Stack, Text, Button } from '@trycompai/design-system'; import { formatDistanceToNow } from 'date-fns'; -import { ChevronDown } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { toast } from 'sonner'; +import { CheckmarkFilled, ChevronDown, CopyToClipboard } from '@trycompai/design-system/icons'; +import { useCallback, useMemo, useState } from 'react'; type AutomationRunWithName = EvidenceAutomationRun & { evidenceAutomation: { @@ -32,6 +33,39 @@ const getStatusStyles = (status: EvidenceAutomationRunStatus) => { } }; +function CopyableCodeBlock({ label, content }: { label: string; content: unknown }) { + const [copied, setCopied] = useState(false); + const text = typeof content === 'string' ? content : JSON.stringify(content, null, 2); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(text); + setCopied(true); + toast.success('Copied to clipboard'); + setTimeout(() => setCopied(false), 2000); + }, [text]); + + return ( +
+ {label} +
+
+ +
+
+          {text}
+        
+
+
+ ); +} + export function AutomationRunsCard({ runs }: AutomationRunsCardProps) { const [expandedId, setExpandedId] = useState(null); const [showAll, setShowAll] = useState(false); @@ -81,11 +115,13 @@ export function AutomationRunsCard({ runs }: AutomationRunsCardProps) {
hasDetails && setExpandedId(isExpanded ? null : run.id)} - role={hasDetails ? 'button' : undefined} - style={hasDetails ? { cursor: 'pointer' } : undefined} > -
+
hasDetails && setExpandedId(isExpanded ? null : run.id)} + role={hasDetails ? 'button' : undefined} + style={hasDetails ? { cursor: 'pointer' } : undefined} + >
@@ -124,12 +160,12 @@ export function AutomationRunsCard({ runs }: AutomationRunsCardProps) {
{hasDetails && ( - + )}
{isExpanded && ( -
+
{run.evaluationReason && (
Evaluation @@ -137,20 +173,10 @@ export function AutomationRunsCard({ runs }: AutomationRunsCardProps) {
)} {run.logs && ( -
- Logs -
-                            {typeof run.logs === 'string' ? run.logs : JSON.stringify(run.logs, null, 2)}
-                          
-
+ )} {run.output && ( -
- Output -
-                            {typeof run.output === 'string' ? run.output : JSON.stringify(run.output, null, 2)}
-                          
-
+ )} {run.status === 'failed' && run.error && (