Skip to content

Commit ceb9bea

Browse files
committed
feat(config): add comprehensive API URL validation across all layers
Add validation for NEXT_PUBLIC_API_URL at multiple levels to catch configuration errors early: - Build-time: Validate URL format and presence in Dockerfile - Runtime: Validate URL format in docker-entrypoint.sh - Frontend: Add URL validation with proper error handling and fallbacks - Infrastructure: Validate URL format in CDK construct - CI/CD: Pass API URL as explicit build argument Additionally, fix logout functionality to use auth context action instead of simple navigation link.
1 parent f47790f commit ceb9bea

7 files changed

Lines changed: 118 additions & 7 deletions

File tree

.github/workflows/deploy-infrastructure.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ jobs:
175175
file: ./frontend/Dockerfile.prod
176176
platforms: linux/arm64
177177
push: true
178+
build-args: |
179+
NEXT_PUBLIC_API_URL=${{ secrets.FASTAPI_BACKEND || 'https://d3cp7cujulcncl.cloudfront.net' }}
178180
tags: |
179181
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_FRONTEND_REPO }}:${{ github.sha }}
180182
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_FRONTEND_REPO }}:latest

frontend/Dockerfile.prod

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,20 @@ WORKDIR /app
1212
ENV NODE_ENV=production
1313
ENV NEXT_TELEMETRY_DISABLED=1
1414
# API URL will be injected via build-arg from CI/CD (GitHub Secrets)
15-
ARG NEXT_PUBLIC_API_URL=https://d3cp7cujulcncl.cloudfront.net
15+
ARG NEXT_PUBLIC_API_URL
16+
17+
# Validate that NEXT_PUBLIC_API_URL is provided
18+
RUN if [ -z "$NEXT_PUBLIC_API_URL" ]; then \
19+
echo "Error: NEXT_PUBLIC_API_URL build-arg is required but not set"; \
20+
exit 1; \
21+
fi
22+
23+
# Validate URL format
24+
RUN if ! echo "$NEXT_PUBLIC_API_URL" | grep -E '^https?://[^ ]+$' > /dev/null; then \
25+
echo "Error: Invalid NEXT_PUBLIC_API_URL format: $NEXT_PUBLIC_API_URL"; \
26+
exit 1; \
27+
fi
28+
1629
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
1730
COPY --from=deps /app/node_modules ./node_modules
1831
COPY . .

frontend/components/dashboard-layout.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { cn } from "@/lib/utils"
55
import Link from "next/link"
66
import { usePathname } from "next/navigation"
77
import { MicSparklesIcon } from "@/components/ui/mic-sparkles-icon"
8+
import { useAuth } from "@/lib/auth/auth-context"
89

910
export default function DashboardLayout({
1011
children,
1112
}: {
1213
children: React.ReactNode
1314
}) {
1415
const pathname = usePathname()
16+
const { logout } = useAuth()
1517

1618
const links = [
1719
{
@@ -31,8 +33,9 @@ export default function DashboardLayout({
3133
},
3234
{
3335
label: "Logout",
34-
href: "/",
36+
href: "#",
3537
icon: IconLogout,
38+
action: logout,
3639
},
3740
]
3841

@@ -59,6 +62,28 @@ export default function DashboardLayout({
5962
{links.map((link, idx) => {
6063
const Icon = link.icon
6164
const active = isActive(link.href)
65+
66+
if (link.action) {
67+
// For logout button
68+
return (
69+
<button
70+
key={idx}
71+
onClick={link.action}
72+
className={cn(
73+
"h-10 w-10 rounded-xl flex items-center justify-center transition-all group relative",
74+
"text-neutral-400 hover:text-white hover:bg-neutral-800/50"
75+
)}
76+
title={link.label}
77+
>
78+
<Icon className="h-5 w-5 shrink-0" />
79+
{/* Tooltip */}
80+
<span className="absolute left-14 px-2 py-1 bg-neutral-800 text-white text-xs rounded-md whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">
81+
{link.label}
82+
</span>
83+
</button>
84+
)
85+
}
86+
6287
return (
6388
<Link
6489
key={idx}

frontend/docker-entrypoint.sh

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
#!/bin/sh
22
set -e
33

4+
# Function to validate URL format
5+
validate_url() {
6+
if ! echo "$1" | grep -E '^https?://[^ ]+$' > /dev/null; then
7+
echo "Error: Invalid URL format: $1"
8+
exit 1
9+
fi
10+
}
11+
412
# Replace placeholder with actual API URL at runtime
513
if [ -n "$NEXT_PUBLIC_API_URL" ]; then
614
echo "Configuring API URL: $NEXT_PUBLIC_API_URL"
715

16+
# Validate URL format
17+
validate_url "$NEXT_PUBLIC_API_URL"
18+
819
# Update the config.js file with the actual API URL
920
cat > /app/public/config.js <<EOF
10-
window.NEXT_PUBLIC_API_URL = '$NEXT_PUBLIC_API_URL';
21+
// Runtime configuration - loaded by browser
22+
(function() {
23+
window.NEXT_PUBLIC_API_URL = '$NEXT_PUBLIC_API_URL';
24+
})();
1125
EOF
26+
27+
echo "API URL configuration completed successfully"
28+
else
29+
echo "Warning: NEXT_PUBLIC_API_URL not set, using build-time configuration"
1230
fi
1331

1432
# Execute the CMD

frontend/lib/api/config.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22
* Centralized API configuration that handles runtime URL injection
33
*/
44

5+
/**
6+
* Validates if a URL is properly formatted
7+
*/
8+
function isValidUrl(url: string): boolean {
9+
try {
10+
new URL(url);
11+
return true;
12+
} catch {
13+
return false;
14+
}
15+
}
16+
517
/**
618
* Get the API base URL from runtime config or build-time environment variable.
719
* This function checks multiple sources in order of precedence:
@@ -14,12 +26,42 @@ export function getApiBaseUrl(): string {
1426
if (typeof window !== 'undefined' && (window as any).NEXT_PUBLIC_API_URL) {
1527
const runtimeUrl = (window as any).NEXT_PUBLIC_API_URL;
1628
// Don't use placeholder value
17-
if (runtimeUrl !== '__PLACEHOLDER__') {
29+
if (runtimeUrl !== '__PLACEHOLDER__' && isValidUrl(runtimeUrl)) {
1830
return runtimeUrl;
1931
}
32+
// If runtime config is invalid, log a warning
33+
if (runtimeUrl !== '__PLACEHOLDER__') {
34+
console.warn(`Invalid runtime API URL: ${runtimeUrl}. Falling back to build-time configuration.`);
35+
}
36+
}
37+
38+
// Fallback to build-time environment variable
39+
const buildTimeUrl = process.env.NEXT_PUBLIC_API_URL;
40+
if (buildTimeUrl && isValidUrl(buildTimeUrl)) {
41+
return buildTimeUrl;
42+
}
43+
44+
// Final fallback to localhost for development
45+
const fallbackUrl = 'http://localhost:8000';
46+
if (!buildTimeUrl) {
47+
console.warn(`No API URL configured. Using fallback: ${fallbackUrl}`);
48+
} else {
49+
console.warn(`Invalid build-time API URL: ${buildTimeUrl}. Using fallback: ${fallbackUrl}`);
2050
}
2151

22-
// Fallback to build-time environment variable or localhost for dev
23-
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
52+
return fallbackUrl;
2453
}
2554

55+
/**
56+
* Get the API base URL with validation and error handling
57+
* Throws an error if no valid URL can be determined
58+
*/
59+
export function getValidatedApiBaseUrl(): string {
60+
const url = getApiBaseUrl();
61+
62+
if (!isValidUrl(url)) {
63+
throw new Error(`Invalid API URL configuration: ${url}`);
64+
}
65+
66+
return url;
67+
}

frontend/public/config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
// Runtime configuration - loaded by browser
2-
window.NEXT_PUBLIC_API_URL = window.NEXT_PUBLIC_API_URL || '__PLACEHOLDER__';
2+
(function() {
3+
// Initialize with a default that will be replaced by docker-entrypoint.sh
4+
if (!window.NEXT_PUBLIC_API_URL) {
5+
window.NEXT_PUBLIC_API_URL = '__PLACEHOLDER__';
6+
}
7+
})();

infrastructure/cdk_constructs/ecs_frontend.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ def __init__(
3333
) -> None:
3434
super().__init__(scope, construct_id, **kwargs)
3535
stack = Stack.of(self)
36+
37+
# Validate backend API URL format
38+
import re
39+
if not re.match(r'^https?://[^ ]+$', backend_api_url):
40+
raise ValueError(f"Invalid backend_api_url format: {backend_api_url}. Must be a valid HTTP/HTTPS URL.")
3641

3742
vpc = ec2.Vpc(self, "FrontendVpc", nat_gateways=1)
3843
cluster = ecs.Cluster(self, "FrontendCluster", vpc=vpc)
@@ -75,6 +80,7 @@ def __init__(
7580
container_port=3000,
7681
environment={
7782
"NEXT_PUBLIC_API_URL": backend_api_url,
83+
"NODE_ENV": "production",
7884
},
7985
),
8086
)

0 commit comments

Comments
 (0)