Convert to Next.js with security features
- Migrate from Vite to Next.js 15 for server-side API support - Add dynamic API route at /api/filaments that fetches from Confluence - Implement security measures: - API credentials only accessible server-side - Security scan script to detect credential leaks - Tests to ensure no sensitive data exposure - Build-time security checks in CI/CD - Update AWS Amplify configuration for Next.js deployment - Update Terraform to use WEB_COMPUTE platform for Next.js - Add Jest tests for API security - Remove static JSON approach in favor of dynamic API This provides real-time data updates while keeping credentials secure on the server. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
37
app/api/filaments/route.ts
Normal file
37
app/api/filaments/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { fetchFromConfluence } from '../../../src/server/confluence';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Get environment variables from server-side only
|
||||
const env = {
|
||||
CONFLUENCE_API_URL: process.env.CONFLUENCE_API_URL,
|
||||
CONFLUENCE_TOKEN: process.env.CONFLUENCE_TOKEN,
|
||||
CONFLUENCE_PAGE_ID: process.env.CONFLUENCE_PAGE_ID,
|
||||
};
|
||||
|
||||
// Validate environment variables
|
||||
if (!env.CONFLUENCE_API_URL || !env.CONFLUENCE_TOKEN || !env.CONFLUENCE_PAGE_ID) {
|
||||
console.error('Missing Confluence environment variables');
|
||||
return NextResponse.json(
|
||||
{ error: 'Server configuration error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const filaments = await fetchFromConfluence(env);
|
||||
|
||||
return NextResponse.json(filaments, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
// Never expose internal error details to client
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch filaments' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
19
app/layout.tsx
Normal file
19
app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next'
|
||||
import '../src/styles/index.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Filamenteka',
|
||||
description: 'Automatsko praćenje filamenata sa kodiranjem bojama',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="sr">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
99
app/page.tsx
Normal file
99
app/page.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FilamentTable } from '../src/components/FilamentTable';
|
||||
import { Filament } from '../src/types/filament';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function Home() {
|
||||
const [filaments, setFilaments] = useState<Filament[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||
const [darkMode, setDarkMode] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('darkMode');
|
||||
return saved ? JSON.parse(saved) : false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('darkMode', JSON.stringify(darkMode));
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
const fetchFilaments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await axios.get('/api/filaments');
|
||||
setFilaments(response.data);
|
||||
setLastUpdate(new Date());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Greška pri učitavanju filamenata');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchFilaments();
|
||||
|
||||
// Refresh every 5 minutes
|
||||
const interval = setInterval(fetchFilaments, 5 * 60 * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
<header className="bg-white dark:bg-gray-800 shadow transition-colors">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Filamenteka
|
||||
</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
{lastUpdate && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Poslednje ažuriranje: {lastUpdate.toLocaleTimeString('sr-RS')}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={fetchFilaments}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Ažuriranje...' : 'Osveži'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
||||
title={darkMode ? 'Svetla tema' : 'Tamna tema'}
|
||||
>
|
||||
{darkMode ? '☀️' : '🌙'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<FilamentTable
|
||||
filaments={filaments}
|
||||
loading={loading}
|
||||
error={error || undefined}
|
||||
/>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user