fix(auth,api): resolve SSR vs CSR API URL routing and sanitize ADMIN_TOKEN parsing
- Implement getApiBaseUrl() and getAdminApiBaseUrl() to dynamically distinguish between Server/SSR (process.env.API_BASE_URL) and Client/CSR (process.env.NEXT_PUBLIC_API_BASE_URL) - Fix missing data on main landing page in containerized environment - Sanitize and trim ADMIN_TOKEN parsing in Go backend to prevent whitespace/quote mismatch - Real-time token validation in AdminLayout with auto-redirection on token mismatch - All unit and E2E test gates passed clean with unanimous PASS reviews
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.godopu.com/lab/landing_page/backend/internal/config"
|
||||
@@ -37,7 +38,7 @@ func main() {
|
||||
cfg.GinMode = *ginModeFlag
|
||||
}
|
||||
if *adminTokenFlag != "" {
|
||||
cfg.AdminToken = *adminTokenFlag
|
||||
cfg.AdminToken = strings.Trim(*adminTokenFlag, " \"'\r\n\t")
|
||||
}
|
||||
|
||||
if cfg.AdminToken == "" {
|
||||
|
||||
@@ -94,7 +94,7 @@ func Load() *Config {
|
||||
}
|
||||
|
||||
if token := os.Getenv("ADMIN_TOKEN"); token != "" {
|
||||
cfg.AdminToken = token
|
||||
cfg.AdminToken = strings.Trim(token, " \"'\r\n\t")
|
||||
}
|
||||
|
||||
if autoMigrateStr := os.Getenv("AUTO_MIGRATE"); autoMigrateStr != "" {
|
||||
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
|
||||
// RequireAdminToken returns a Gin middleware that validates Bearer <token> against the configured admin token.
|
||||
func RequireAdminToken(adminToken string) gin.HandlerFunc {
|
||||
expectedToken := strings.Trim(adminToken, " \"'\r\n\t")
|
||||
return func(c *gin.Context) {
|
||||
if adminToken == "" {
|
||||
if expectedToken == "" {
|
||||
Unauthorized(c, "Admin API is disabled: ADMIN_TOKEN is not configured")
|
||||
c.Abort()
|
||||
return
|
||||
@@ -23,7 +24,7 @@ func RequireAdminToken(adminToken string) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || parts[1] != adminToken {
|
||||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || strings.TrimSpace(parts[1]) != expectedToken {
|
||||
Unauthorized(c, "Invalid admin bearer token")
|
||||
c.Abort()
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { getClientToken, removeClientToken } from "./_components/AdminComponents";
|
||||
import { adminVerifyToken } from "../../lib/adminApi";
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
@@ -11,12 +12,37 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getClientToken();
|
||||
if (!token && pathname !== "/console/login") {
|
||||
router.push("/console/login");
|
||||
} else {
|
||||
setIsAuthenticated(true);
|
||||
let isMounted = true;
|
||||
async function checkAuth() {
|
||||
const token = getClientToken();
|
||||
if (!token) {
|
||||
if (pathname !== "/console/login") {
|
||||
router.push("/console/login");
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await adminVerifyToken(token);
|
||||
if (!isMounted) return;
|
||||
|
||||
if (!isValid) {
|
||||
removeClientToken();
|
||||
if (pathname !== "/console/login") {
|
||||
router.push("/console/login");
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
} else {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
}
|
||||
|
||||
checkAuth();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [pathname, router]);
|
||||
|
||||
if (pathname === "/console/login") {
|
||||
|
||||
@@ -3,10 +3,22 @@
|
||||
* Preserves database IDs and handles authenticated mutating requests (POST, PUT, DELETE).
|
||||
*/
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
process.env.API_BASE_URL ||
|
||||
"http://localhost:8080/api/v1";
|
||||
export function getAdminApiBaseUrl(): string {
|
||||
if (typeof window !== "undefined") {
|
||||
// Browser / CSR context: use public endpoint
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
process.env.API_BASE_URL ||
|
||||
"http://localhost:8080/api/v1"
|
||||
);
|
||||
}
|
||||
// Server / SSR context: prefer internal container DNS if available, fallback to NEXT_PUBLIC or localhost
|
||||
return (
|
||||
process.env.API_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
"http://localhost:8080/api/v1"
|
||||
);
|
||||
}
|
||||
|
||||
export interface AdminResearchProject {
|
||||
id: number;
|
||||
@@ -143,7 +155,8 @@ async function request<T>(
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
const baseUrl = getAdminApiBaseUrl();
|
||||
const url = `${baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
|
||||
@@ -10,7 +10,23 @@ import type {
|
||||
DocStatus,
|
||||
} from "../content/types";
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL ?? "http://localhost:8080/api/v1";
|
||||
export function getApiBaseUrl(): string {
|
||||
if (typeof window !== "undefined") {
|
||||
// Browser / CSR context: use public endpoint
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
process.env.API_BASE_URL ||
|
||||
"http://localhost:8080/api/v1"
|
||||
);
|
||||
}
|
||||
// Server / SSR context: prefer internal container DNS if available, fallback to NEXT_PUBLIC or localhost
|
||||
return (
|
||||
process.env.API_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
"http://localhost:8080/api/v1"
|
||||
);
|
||||
}
|
||||
|
||||
const FETCH_TIMEOUT_MS = 3000;
|
||||
|
||||
interface APIResponse<T> {
|
||||
@@ -142,7 +158,8 @@ async function apiGet<T>(path: string): Promise<T | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
const res = await fetch(`${API_BASE_URL}${path}`, {
|
||||
const baseUrl = getApiBaseUrl();
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
signal: controller.signal,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user