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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,6 +9,8 @@ coverage/
|
||||
# Production
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
|
||||
59
__tests__/security.test.ts
Normal file
59
__tests__/security.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Mock Next.js server components
|
||||
jest.mock('next/server', () => ({
|
||||
NextResponse: {
|
||||
json: (data: any, init?: ResponseInit) => ({
|
||||
json: async () => data,
|
||||
...init
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock confluence module
|
||||
jest.mock('../src/server/confluence', () => ({
|
||||
fetchFromConfluence: jest.fn()
|
||||
}));
|
||||
|
||||
import { GET } from '../app/api/filaments/route';
|
||||
import { fetchFromConfluence } from '../src/server/confluence';
|
||||
|
||||
describe('API Security Tests', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should not expose credentials in error responses', async () => {
|
||||
// Simulate missing environment variables
|
||||
delete process.env.CONFLUENCE_TOKEN;
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
// Check that response doesn't contain sensitive information
|
||||
expect(JSON.stringify(data)).not.toContain('ATATT');
|
||||
expect(JSON.stringify(data)).not.toContain('token');
|
||||
expect(JSON.stringify(data)).not.toContain('password');
|
||||
expect(data.error).toBe('Server configuration error');
|
||||
});
|
||||
|
||||
it('should not expose internal error details', async () => {
|
||||
// Set invalid environment to trigger error
|
||||
process.env.CONFLUENCE_API_URL = 'invalid-url';
|
||||
process.env.CONFLUENCE_TOKEN = 'test-token';
|
||||
process.env.CONFLUENCE_PAGE_ID = 'test-page';
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
// Should get generic error, not specific details
|
||||
expect(data.error).toBe('Failed to fetch filaments');
|
||||
expect(data).not.toHaveProperty('stack');
|
||||
expect(data).not.toHaveProperty('message');
|
||||
});
|
||||
});
|
||||
@@ -4,14 +4,16 @@ frontend:
|
||||
preBuild:
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run security:check
|
||||
build:
|
||||
commands:
|
||||
- npx tsx scripts/build-data.js
|
||||
- npm run build
|
||||
- npm run test
|
||||
artifacts:
|
||||
baseDirectory: dist
|
||||
baseDirectory: .next
|
||||
files:
|
||||
- '**/*'
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/**/*
|
||||
- .next/cache/**/*
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FilamentTable } from './components/FilamentTable';
|
||||
import { Filament } from './types/filament';
|
||||
import { FilamentTable } from '../src/components/FilamentTable';
|
||||
import { Filament } from '../src/types/filament';
|
||||
import axios from 'axios';
|
||||
|
||||
function App() {
|
||||
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 () => {
|
||||
@@ -27,9 +34,7 @@ function App() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// In development, use the API endpoint; in production, use the static JSON
|
||||
const url = import.meta.env.DEV ? '/api/filaments' : '/filaments.json';
|
||||
const response = await axios.get(url);
|
||||
const response = await axios.get('/api/filaments');
|
||||
setFilaments(response.data);
|
||||
setLastUpdate(new Date());
|
||||
} catch (err) {
|
||||
@@ -92,5 +97,3 @@ function App() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
13
index.html
13
index.html
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Filamenteka - Bambu Lab Filament Tracker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
20
jest.config.js
Normal file
20
jest.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const nextJest = require('next/jest')
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
})
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const customJestConfig = {
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/$1',
|
||||
},
|
||||
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
|
||||
modulePathIgnorePatterns: ['<rootDir>/.next/'],
|
||||
}
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig)
|
||||
1
jest.setup.js
Normal file
1
jest.setup.js
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom'
|
||||
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
7
next.config.js
Normal file
7
next.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
5036
package-lock.json
generated
5036
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -2,21 +2,27 @@
|
||||
"name": "filamenteka",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"security:check": "node scripts/security-check.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cheerio": "^0.22.35",
|
||||
"axios": "^1.6.2",
|
||||
"cheerio": "^1.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"next": "^15.3.4",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
@@ -26,6 +32,8 @@
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"jest": "^30.0.0",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"tsx": "^4.20.3",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
[
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Mistletoe Green",
|
||||
"refill": "",
|
||||
"vakum": "vakuum x1",
|
||||
"otvoreno": "otvorena x1",
|
||||
"kolicina": "2",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Indingo Purple",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Black",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "2x otvorena",
|
||||
"kolicina": "2",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Black",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Jade White",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Gray",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Red",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Hot Pink",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Cocoa Brown",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "White",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Coton Candy Cloud",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Sunflower Yellow",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Yellow",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Magenta",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Beige",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Cyan",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Scarlet Red",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Mandarin Orange",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Marine Blue",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Charcoal",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Ivory White",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Ivory White",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Ash Gray",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Cobalt Blue",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Turquoise",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Nardo Gray",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Bright Green",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Matte",
|
||||
"boja": "Charcoal",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Basic",
|
||||
"boja": "Gold",
|
||||
"refill": "Da",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Glow",
|
||||
"boja": "Glow Green",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "Wood",
|
||||
"boja": "Black Walnut",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "CF",
|
||||
"boja": "Black",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PLA",
|
||||
"finish": "CF",
|
||||
"boja": "Jeans Blue",
|
||||
"refill": "",
|
||||
"vakum": "",
|
||||
"otvoreno": "otvorena",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "PETG",
|
||||
"finish": "",
|
||||
"boja": "Black",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
},
|
||||
{
|
||||
"brand": "BambuLab",
|
||||
"tip": "ABS",
|
||||
"finish": "",
|
||||
"boja": "Black",
|
||||
"refill": "",
|
||||
"vakum": "vakuum",
|
||||
"otvoreno": "",
|
||||
"kolicina": "",
|
||||
"cena": ""
|
||||
}
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { fetchFromConfluence } from '../src/server/confluence.ts';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
async function buildData() {
|
||||
try {
|
||||
console.log('Fetching filament data from Confluence...');
|
||||
const filaments = await fetchFromConfluence({
|
||||
CONFLUENCE_API_URL: process.env.CONFLUENCE_API_URL,
|
||||
CONFLUENCE_TOKEN: process.env.CONFLUENCE_TOKEN,
|
||||
CONFLUENCE_PAGE_ID: process.env.CONFLUENCE_PAGE_ID
|
||||
});
|
||||
|
||||
// Create public directory if it doesn't exist
|
||||
const publicDir = path.join(__dirname, '..', 'public');
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write data to public directory
|
||||
const dataPath = path.join(publicDir, 'filaments.json');
|
||||
fs.writeFileSync(dataPath, JSON.stringify(filaments, null, 2));
|
||||
|
||||
console.log(`Successfully wrote ${filaments.length} filaments to ${dataPath}`);
|
||||
} catch (error) {
|
||||
console.error('Error building data:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
buildData();
|
||||
91
scripts/security-check.js
Normal file
91
scripts/security-check.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Patterns that might indicate leaked credentials
|
||||
const sensitivePatterns = [
|
||||
/ATATT[A-Za-z0-9+/=]{100,}/g, // Confluence tokens
|
||||
/ghp_[A-Za-z0-9]{36,}/g, // GitHub tokens
|
||||
/api[_-]?key[_-]?[=:]\s*["']?[A-Za-z0-9+/=]{20,}/gi,
|
||||
/token[_-]?[=:]\s*["']?[A-Za-z0-9+/=]{20,}/gi,
|
||||
/password[_-]?[=:]\s*["']?[^\s"']{8,}/gi,
|
||||
/secret[_-]?[=:]\s*["']?[A-Za-z0-9+/=]{20,}/gi,
|
||||
];
|
||||
|
||||
// Files to exclude from scanning
|
||||
const excludePatterns = [
|
||||
/node_modules/,
|
||||
/\.git/,
|
||||
/\.next/,
|
||||
/dist/,
|
||||
/build/,
|
||||
/terraform\.tfvars$/,
|
||||
/\.env/,
|
||||
/security-check\.js$/,
|
||||
];
|
||||
|
||||
function scanFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const issues = [];
|
||||
|
||||
sensitivePatterns.forEach(pattern => {
|
||||
const matches = content.match(pattern);
|
||||
if (matches) {
|
||||
matches.forEach(match => {
|
||||
// Only flag if it's not a placeholder or example
|
||||
if (!match.includes('example') && !match.includes('YOUR_') && !match.includes('xxx')) {
|
||||
issues.push({
|
||||
file: filePath,
|
||||
pattern: pattern.source,
|
||||
match: match.substring(0, 20) + '...',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function scanDirectory(dir) {
|
||||
const issues = [];
|
||||
|
||||
function walk(currentPath) {
|
||||
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry.name);
|
||||
const relativePath = path.relative(process.cwd(), fullPath);
|
||||
|
||||
// Skip excluded paths
|
||||
if (excludePatterns.some(pattern => pattern.test(relativePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath);
|
||||
} else if (entry.isFile() && /\.(js|ts|jsx|tsx|json|yml|yaml|md)$/.test(entry.name)) {
|
||||
const fileIssues = scanFile(fullPath);
|
||||
issues.push(...fileIssues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(dir);
|
||||
return issues;
|
||||
}
|
||||
|
||||
// Run the scan
|
||||
console.log('🔍 Scanning for potential credential leaks...\n');
|
||||
const issues = scanDirectory(process.cwd());
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.error('❌ Found potential credential leaks:\n');
|
||||
issues.forEach(issue => {
|
||||
console.error(`File: ${issue.file}`);
|
||||
console.error(`Pattern: ${issue.pattern}`);
|
||||
console.error(`Match: ${issue.match}\n`);
|
||||
});
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ No credential leaks detected');
|
||||
}
|
||||
10
src/main.tsx
10
src/main.tsx
@@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './styles/index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -1,9 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
@@ -15,11 +15,12 @@ provider "aws" {
|
||||
resource "aws_amplify_app" "filamenteka" {
|
||||
name = "filamenteka"
|
||||
repository = var.github_repository
|
||||
platform = "WEB_COMPUTE"
|
||||
|
||||
# GitHub access token for private repos
|
||||
access_token = var.github_token
|
||||
|
||||
# Build settings
|
||||
# Build settings for Next.js
|
||||
build_spec = <<-EOT
|
||||
version: 1
|
||||
frontend:
|
||||
@@ -27,16 +28,19 @@ resource "aws_amplify_app" "filamenteka" {
|
||||
preBuild:
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run security:check
|
||||
build:
|
||||
commands:
|
||||
- npm run build
|
||||
- npm run test
|
||||
artifacts:
|
||||
baseDirectory: dist
|
||||
baseDirectory: .next
|
||||
files:
|
||||
- '**/*'
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/**/*
|
||||
- .next/cache/**/*
|
||||
EOT
|
||||
|
||||
# Environment variables
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// Dynamic import to avoid TypeScript issues
|
||||
async function getConfluenceData(env: any) {
|
||||
const { fetchFromConfluence } = await import('./src/server/confluence.js')
|
||||
return fetchFromConfluence(env)
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
publicDir: 'public',
|
||||
plugins: [
|
||||
react(),
|
||||
{
|
||||
name: 'api-middleware',
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/api/filaments', async (req, res, next) => {
|
||||
if (req.method !== 'GET') {
|
||||
res.statusCode = 405
|
||||
res.end('Method not allowed')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const filaments = await getConfluenceData(env)
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Cache-Control', 'max-age=300')
|
||||
res.end(JSON.stringify(filaments))
|
||||
} catch (error) {
|
||||
console.error('API Error:', error)
|
||||
res.statusCode = 500
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({
|
||||
error: 'Failed to fetch filaments',
|
||||
message: error instanceof Error ? error.message : 'Unknown error'
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user