Fix API connectivity and import filament data from PDF

- Update all environment files to use new PostgreSQL API endpoint
- Fix CORS configuration in API server
- Import 35 filaments and 29 colors from PDF data
- Fix TypeScript type error in dashboard
- Add back emoji icons for dark mode toggle
- Remove debugging code and test buttons
- Clean up error handling
This commit is contained in:
DaX
2025-06-20 15:40:40 +02:00
parent 62a4891112
commit 82c476430f
13 changed files with 737 additions and 29 deletions

View File

@@ -2,11 +2,11 @@
NODE_ENV=development
# API Configuration
NEXT_PUBLIC_API_URL=https://5wmfjjzm0i.execute-api.eu-central-1.amazonaws.com/dev
NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api
# AWS Configuration
AWS_REGION=eu-central-1
DYNAMODB_TABLE_NAME=filamenteka-filaments-dev
# AWS Configuration (for reference - no longer used)
# AWS_REGION=eu-central-1
# DYNAMODB_TABLE_NAME=filamenteka-filaments-dev
# Admin credentials (development)
# Username: admin

View File

@@ -2,10 +2,8 @@
NODE_ENV=production
# API Configuration
NEXT_PUBLIC_API_URL=https://5wmfjjzm0i.execute-api.eu-central-1.amazonaws.com/production
NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api
# AWS Configuration
AWS_REGION=eu-central-1
DYNAMODB_TABLE_NAME=filamenteka-filaments
# Admin credentials are stored in AWS Secrets Manager in production
# AWS Configuration (for reference - no longer used)
# AWS_REGION=eu-central-1
# DYNAMODB_TABLE_NAME=filamenteka-filaments

View File

@@ -19,10 +19,15 @@ app.use(cors({
origin: true, // Allow all origins in development
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['Content-Length', 'Content-Type'],
maxAge: 86400
}));
app.use(express.json());
// Handle preflight requests
app.options('*', cors());
// Health check route
app.get('/', (req, res) => {
res.json({ status: 'ok', service: 'Filamenteka API' });
@@ -34,11 +39,14 @@ const authenticateToken = (req, res, next) => {
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
return res.status(401).json({ error: 'Unauthorized' });
}
jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key', (err, user) => {
if (err) return res.sendStatus(403);
if (err) {
console.error('JWT verification error:', err);
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
@@ -111,8 +119,9 @@ app.delete('/api/colors/:id', authenticateToken, async (req, res) => {
}
});
// Filaments endpoints
// Filaments endpoints (PUBLIC - no auth required)
app.get('/api/filaments', async (req, res) => {
console.log('Filaments request headers:', req.headers);
try {
const result = await pool.query('SELECT * FROM filaments ORDER BY created_at DESC');
res.json(result.rows);

View File

@@ -42,9 +42,19 @@ export default function Home() {
const filaments = await filamentService.getAll();
setFilaments(filaments);
} catch (err) {
} catch (err: any) {
console.error('API Error:', err);
setError(err instanceof Error ? err.message : 'Greška pri učitavanju filamenata');
// More descriptive error messages
if (err.code === 'ERR_NETWORK') {
setError('Network Error - Unable to connect to API');
} else if (err.response) {
setError(`Server error: ${err.response.status} - ${err.response.statusText}`);
} else if (err.request) {
setError('No response from server - check if API is running');
} else {
setError(err.message || 'Greška pri učitavanju filamenata');
}
} finally {
setLoading(false);
}
@@ -91,7 +101,7 @@ export default function Home() {
className="p-2 bg-white/50 dark:bg-gray-700/50 backdrop-blur text-gray-800 dark:text-gray-200 rounded-full hover:bg-white/80 dark:hover:bg-gray-600/80 transition-all duration-200 hover:scale-110 shadow-md ml-2"
title={darkMode ? 'Svetla tema' : 'Tamna tema'}
>
{darkMode ? 'Svetla' : 'Tamna'}
{darkMode ? '☀️' : '🌙'}
</button>
)}
</div>

View File

@@ -2,8 +2,8 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { colorService } from '../../../src/services/api';
import '../../../src/styles/select.css';
import { colorService } from '@/src/services/api';
import '@/src/styles/select.css';
interface Color {
id: string;
@@ -170,7 +170,7 @@ export default function ColorsManagement() {
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 ? 'Svetla' : 'Tamna'}
{darkMode ? '☀️' : '🌙'}
</button>
)}
<button

View File

@@ -2,9 +2,9 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { filamentService, colorService } from '../../../src/services/api';
import { Filament } from '../../../src/types/filament';
import '../../../src/styles/select.css';
import { filamentService, colorService } from '@/src/services/api';
import { Filament } from '@/src/types/filament';
import '@/src/styles/select.css';
interface FilamentWithId extends Filament {
id: string;
@@ -188,7 +188,7 @@ export default function AdminDashboard() {
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 ? 'Svetla' : 'Tamna'}
{darkMode ? '☀️' : '🌙'}
</button>
)}
<button
@@ -502,7 +502,7 @@ function FilamentForm({
name="bojaHex"
value={formData.bojaHex || '#000000'}
onChange={handleChange}
disabled={formData.boja && formData.boja !== 'custom' && availableColors.some(c => c.name === formData.boja)}
disabled={!!(formData.boja && formData.boja !== 'custom' && availableColors.some(c => c.name === formData.boja))}
className="w-full h-10 px-1 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
/>
{formData.bojaHex && (

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { authService } from '../../src/services/api';
import { authService } from '@/src/services/api';
export default function AdminLogin() {
const router = useRouter();
@@ -30,7 +30,7 @@ export default function AdminLogin() {
// Redirect to admin dashboard
router.push('/upadaj/dashboard');
} catch (err) {
} catch (err: any) {
setError('Neispravno korisničko ime ili lozinka');
console.error('Login error:', err);
} finally {

422
data.json Normal file
View File

@@ -0,0 +1,422 @@
[
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Mistletoe Green",
"boja_hex": "#3a5a40",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Da",
"kolicina": 2,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Indingo Purple",
"boja_hex": "#4b0082",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 2,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Jade White",
"boja_hex": "#f0f8ff",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Gray",
"boja_hex": "#808080",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Red",
"boja_hex": "#ff0000",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Hot Pink",
"boja_hex": "#ff69b4",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Cocoa Brown",
"boja_hex": "#d2691e",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "White",
"boja_hex": "#ffffff",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Coton Candy Cloud",
"boja_hex": "#ffb6c1",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Sunflower Yellow",
"boja_hex": "#ffda03",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Yellow",
"boja_hex": "#ffff00",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Magenta",
"boja_hex": "#ff00ff",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Beige",
"boja_hex": "#f5f5dc",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Cyan",
"boja_hex": "#00ffff",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Scarlet Red",
"boja_hex": "#ff2400",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Mandarin Orange",
"boja_hex": "#ff8c00",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Marine Blue",
"boja_hex": "#000080",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Charcoal",
"boja_hex": "#36454f",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Ivory White",
"boja_hex": "#fffff0",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Ivory White",
"boja_hex": "#fffff0",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Ash Gray",
"boja_hex": "#b2beb5",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Cobalt Blue",
"boja_hex": "#0047ab",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Turquoise",
"boja_hex": "#40e0d0",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Nardo Gray",
"boja_hex": "#4d4d4d",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Bright Green",
"boja_hex": "#66ff00",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Matte",
"boja": "Charcoal",
"boja_hex": "#36454f",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Basic",
"boja": "Gold",
"boja_hex": "#ffd700",
"refill": "Da",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Glow",
"boja": "Glow Green",
"boja_hex": "#39ff14",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "Wood",
"boja": "Black Walnut",
"boja_hex": "#5d4e37",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "CF",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PLA",
"finish": "CF",
"boja": "Jeans Blue",
"boja_hex": "#5dadec",
"refill": "Ne",
"vakum": "Ne",
"otvoreno": "Da",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "PETG",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
},
{
"brand": "BambuLab",
"tip": "ABS",
"finish": "Basic",
"boja": "Black",
"boja_hex": "#000000",
"refill": "Ne",
"vakum": "Da",
"otvoreno": "Ne",
"kolicina": 1,
"cena": 0
}
]

0
dynamodb-export.json Normal file
View File

141
import-data.js Normal file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/env node
const fs = require('fs');
const https = require('https');
// Read the data file
const data = JSON.parse(fs.readFileSync('./data.json', 'utf8'));
const uniqueColors = JSON.parse(fs.readFileSync('./unique_colors.json', 'utf8'));
const API_URL = 'https://api.filamenteka.rs/api';
// First, get auth token
async function getAuthToken() {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({
username: process.env.ADMIN_USERNAME || 'admin',
password: process.env.ADMIN_PASSWORD || 'admin'
});
const options = {
hostname: 'api.filamenteka.rs',
path: '/api/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': postData.length
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const response = JSON.parse(data);
resolve(response.token);
} catch (err) {
reject(err);
}
});
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
// Make authenticated request
async function makeRequest(method, path, data, token) {
return new Promise((resolve, reject) => {
const postData = data ? JSON.stringify(data) : '';
const options = {
hostname: 'api.filamenteka.rs',
path: `/api${path}`,
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
};
if (postData) {
options.headers['Content-Length'] = postData.length;
}
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => responseData += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(responseData ? JSON.parse(responseData) : null);
} catch {
resolve(responseData);
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
}
});
});
req.on('error', reject);
if (postData) req.write(postData);
req.end();
});
}
async function importData() {
try {
console.log('Getting auth token...');
const token = await getAuthToken();
console.log('Authentication successful!');
// Import colors first
console.log('\nImporting colors...');
for (const color of uniqueColors) {
try {
await makeRequest('POST', '/colors', {
name: color.name,
hex: color.hex
}, token);
console.log(`✓ Added color: ${color.name}`);
} catch (err) {
if (err.message.includes('already exists')) {
console.log(`⚠ Color already exists: ${color.name}`);
} else {
console.error(`✗ Failed to add color ${color.name}:`, err.message);
}
}
}
// Import filaments
console.log('\nImporting filaments...');
for (const filament of data) {
try {
await makeRequest('POST', '/filaments', {
brand: filament.brand,
tip: filament.tip,
finish: filament.finish,
boja: filament.boja,
bojaHex: filament.boja_hex,
refill: filament.refill,
vakum: filament.vakum,
otvoreno: filament.otvoreno,
kolicina: filament.kolicina.toString(),
cena: filament.cena.toString()
}, token);
console.log(`✓ Added filament: ${filament.brand} ${filament.tip} ${filament.finish} - ${filament.boja}`);
} catch (err) {
console.error(`✗ Failed to add filament:`, err.message);
}
}
console.log('\nData import completed!');
} catch (err) {
console.error('Import failed:', err);
}
}
importData();

View File

@@ -23,7 +23,13 @@ api.interceptors.request.use((config) => {
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401 || error.response?.status === 403) {
// Only redirect to login for protected routes
const protectedPaths = ['/colors', '/filaments'];
const isProtectedRoute = protectedPaths.some(path =>
error.config?.url?.includes(path) && error.config?.method !== 'get'
);
if ((error.response?.status === 401 || error.response?.status === 403) && isProtectedRoute) {
localStorage.removeItem('authToken');
localStorage.removeItem('tokenExpiry');
window.location.href = '/upadaj';

View File

@@ -57,6 +57,7 @@ Requires=docker.service
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/ec2-user
ExecStartPre=/usr/local/bin/docker-compose pull
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
TimeoutStartSec=0
@@ -66,3 +67,6 @@ WantedBy=multi-user.target
EOF
systemctl enable api.service
# Add cron job to update container every 5 minutes
echo "*/5 * * * * cd /home/ec2-user && /usr/bin/aws ecr get-login-password --region ${aws_region} | /usr/bin/docker login --username AWS --password-stdin ${ecr_url} && /usr/local/bin/docker-compose pull && /usr/local/bin/docker-compose up -d" | crontab -

118
unique_colors.json Normal file
View File

@@ -0,0 +1,118 @@
[
{
"name": "Sunflower Yellow",
"hex": "#ffda03"
},
{
"name": "Hot Pink",
"hex": "#ff69b4"
},
{
"name": "Indingo Purple",
"hex": "#4b0082"
},
{
"name": "Coton Candy Cloud",
"hex": "#ffb6c1"
},
{
"name": "Nardo Gray",
"hex": "#4d4d4d"
},
{
"name": "Bright Green",
"hex": "#66ff00"
},
{
"name": "Glow Green",
"hex": "#39ff14"
},
{
"name": "Cocoa Brown",
"hex": "#d2691e"
},
{
"name": "White",
"hex": "#ffffff"
},
{
"name": "Beige",
"hex": "#f5f5dc"
},
{
"name": "Gold",
"hex": "#ffd700"
},
{
"name": "Black",
"hex": "#000000"
},
{
"name": "Cyan",
"hex": "#00ffff"
},
{
"name": "Gray",
"hex": "#808080"
},
{
"name": "Cobalt Blue",
"hex": "#0047ab"
},
{
"name": "Magenta",
"hex": "#ff00ff"
},
{
"name": "Charcoal",
"hex": "#36454f"
},
{
"name": "Mistletoe Green",
"hex": "#3a5a40"
},
{
"name": "Black Walnut",
"hex": "#5d4e37"
},
{
"name": "Jeans Blue",
"hex": "#5dadec"
},
{
"name": "Mandarin Orange",
"hex": "#ff8c00"
},
{
"name": "Ash Gray",
"hex": "#b2beb5"
},
{
"name": "Ivory White",
"hex": "#fffff0"
},
{
"name": "Red",
"hex": "#ff0000"
},
{
"name": "Turquoise",
"hex": "#40e0d0"
},
{
"name": "Scarlet Red",
"hex": "#ff2400"
},
{
"name": "Marine Blue",
"hex": "#000080"
},
{
"name": "Jade White",
"hex": "#f0f8ff"
},
{
"name": "Yellow",
"hex": "#ffff00"
}
]