Fix production environment variables
- Remove old Confluence variables - Add NEXT_PUBLIC_API_URL for API access 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
# This file is for Amplify to know which env vars to expose to Next.js
|
# This file is for Amplify to know which env vars to expose to Next.js
|
||||||
# The actual values come from Amplify Environment Variables
|
# The actual values come from Amplify Environment Variables
|
||||||
CONFLUENCE_API_URL=${CONFLUENCE_API_URL}
|
NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
CONFLUENCE_TOKEN=${CONFLUENCE_TOKEN}
|
|
||||||
CONFLUENCE_PAGE_ID=${CONFLUENCE_PAGE_ID}
|
|
||||||
386
app/admin/dashboard/page.tsx
Normal file
386
app/admin/dashboard/page.tsx
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Filament } from '../../../src/types/filament';
|
||||||
|
|
||||||
|
interface FilamentWithId extends Filament {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminDashboard() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [filaments, setFilaments] = useState<FilamentWithId[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [editingFilament, setEditingFilament] = useState<FilamentWithId | null>(null);
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
|
||||||
|
// Check authentication
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
const expiry = localStorage.getItem('tokenExpiry');
|
||||||
|
|
||||||
|
if (!token || !expiry || Date.now() > parseInt(expiry)) {
|
||||||
|
router.push('/admin');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
// Fetch filaments
|
||||||
|
const fetchFilaments = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/filaments`);
|
||||||
|
setFilaments(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Greška pri učitavanju filamenata');
|
||||||
|
console.error('Fetch error:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchFilaments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getAuthHeaders = () => ({
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('authToken')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (!confirm('Da li ste sigurni da želite obrisati ovaj filament?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`${process.env.NEXT_PUBLIC_API_URL}/filaments/${id}`, getAuthHeaders());
|
||||||
|
await fetchFilaments();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Greška pri brisanju filamenta');
|
||||||
|
console.error('Delete error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (filament: Partial<FilamentWithId>) => {
|
||||||
|
try {
|
||||||
|
if (filament.id) {
|
||||||
|
// Update existing
|
||||||
|
await axios.put(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/filaments/${filament.id}`,
|
||||||
|
filament,
|
||||||
|
getAuthHeaders()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
await axios.post(
|
||||||
|
`${process.env.NEXT_PUBLIC_API_URL}/filaments`,
|
||||||
|
filament,
|
||||||
|
getAuthHeaders()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditingFilament(null);
|
||||||
|
setShowAddForm(false);
|
||||||
|
await fetchFilaments();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Greška pri čuvanju filamenta');
|
||||||
|
console.error('Save error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
localStorage.removeItem('tokenExpiry');
|
||||||
|
router.push('/admin');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 dark:border-gray-100"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
<header className="bg-white dark:bg-gray-800 shadow">
|
||||||
|
<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">
|
||||||
|
Admin Dashboard
|
||||||
|
</h1>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAddForm(true)}
|
||||||
|
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Dodaj novi filament
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Odjava
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-4 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-400 rounded">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add/Edit Form */}
|
||||||
|
{(showAddForm || editingFilament) && (
|
||||||
|
<FilamentForm
|
||||||
|
filament={editingFilament || {}}
|
||||||
|
onSave={handleSave}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditingFilament(null);
|
||||||
|
setShowAddForm(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filaments Table */}
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full bg-white dark:bg-gray-800 shadow rounded">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100 dark:bg-gray-700">
|
||||||
|
<th className="px-4 py-2 text-left">Brand</th>
|
||||||
|
<th className="px-4 py-2 text-left">Tip</th>
|
||||||
|
<th className="px-4 py-2 text-left">Finish</th>
|
||||||
|
<th className="px-4 py-2 text-left">Boja</th>
|
||||||
|
<th className="px-4 py-2 text-left">Refill</th>
|
||||||
|
<th className="px-4 py-2 text-left">Vakum</th>
|
||||||
|
<th className="px-4 py-2 text-left">Otvoreno</th>
|
||||||
|
<th className="px-4 py-2 text-left">Količina</th>
|
||||||
|
<th className="px-4 py-2 text-left">Cena</th>
|
||||||
|
<th className="px-4 py-2 text-left">Akcije</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filaments.map((filament) => (
|
||||||
|
<tr key={filament.id} className="border-t dark:border-gray-700">
|
||||||
|
<td className="px-4 py-2">{filament.brand}</td>
|
||||||
|
<td className="px-4 py-2">{filament.tip}</td>
|
||||||
|
<td className="px-4 py-2">{filament.finish}</td>
|
||||||
|
<td className="px-4 py-2">{filament.boja}</td>
|
||||||
|
<td className="px-4 py-2">{filament.refill}</td>
|
||||||
|
<td className="px-4 py-2">{filament.vakum}</td>
|
||||||
|
<td className="px-4 py-2">{filament.otvoreno}</td>
|
||||||
|
<td className="px-4 py-2">{filament.kolicina}</td>
|
||||||
|
<td className="px-4 py-2">{filament.cena}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingFilament(filament)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 mr-2"
|
||||||
|
>
|
||||||
|
Izmeni
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(filament.id)}
|
||||||
|
className="text-red-600 hover:text-red-800"
|
||||||
|
>
|
||||||
|
Obriši
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filament Form Component
|
||||||
|
function FilamentForm({
|
||||||
|
filament,
|
||||||
|
onSave,
|
||||||
|
onCancel
|
||||||
|
}: {
|
||||||
|
filament: Partial<FilamentWithId>,
|
||||||
|
onSave: (filament: Partial<FilamentWithId>) => void,
|
||||||
|
onCancel: () => void
|
||||||
|
}) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
brand: filament.brand || '',
|
||||||
|
tip: filament.tip || '',
|
||||||
|
finish: filament.finish || '',
|
||||||
|
boja: filament.boja || '',
|
||||||
|
refill: filament.refill || '',
|
||||||
|
vakum: filament.vakum || '',
|
||||||
|
otvoreno: filament.otvoreno || '',
|
||||||
|
kolicina: filament.kolicina || '',
|
||||||
|
cena: filament.cena || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSave({
|
||||||
|
...filament,
|
||||||
|
...formData
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8 p-6 bg-white dark:bg-gray-800 rounded shadow">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{filament.id ? 'Izmeni filament' : 'Dodaj novi filament'}
|
||||||
|
</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Brand</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="brand"
|
||||||
|
value={formData.brand}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Tip</label>
|
||||||
|
<select
|
||||||
|
name="tip"
|
||||||
|
value={formData.tip}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<option value="">Izaberi tip</option>
|
||||||
|
<option value="PLA">PLA</option>
|
||||||
|
<option value="PETG">PETG</option>
|
||||||
|
<option value="ABS">ABS</option>
|
||||||
|
<option value="TPU">TPU</option>
|
||||||
|
<option value="Silk PLA">Silk PLA</option>
|
||||||
|
<option value="PLA Matte">PLA Matte</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Finish</label>
|
||||||
|
<select
|
||||||
|
name="finish"
|
||||||
|
value={formData.finish}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<option value="">Izaberi finish</option>
|
||||||
|
<option value="Basic">Basic</option>
|
||||||
|
<option value="Matte">Matte</option>
|
||||||
|
<option value="Silk">Silk</option>
|
||||||
|
<option value="Metal">Metal</option>
|
||||||
|
<option value="Glow">Glow</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Boja</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="boja"
|
||||||
|
value={formData.boja}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Refill</label>
|
||||||
|
<select
|
||||||
|
name="refill"
|
||||||
|
value={formData.refill}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
>
|
||||||
|
<option value="">Ne</option>
|
||||||
|
<option value="Da">Da</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Vakum</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="vakum"
|
||||||
|
value={formData.vakum}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Otvoreno</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="otvoreno"
|
||||||
|
value={formData.otvoreno}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Količina</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="kolicina"
|
||||||
|
value={formData.kolicina}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Cena</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="cena"
|
||||||
|
value={formData.cena}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2 flex gap-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Sačuvaj
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
Otkaži
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
app/admin/page.tsx
Normal file
104
app/admin/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default function AdminLogin() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store token in localStorage
|
||||||
|
localStorage.setItem('authToken', response.data.token);
|
||||||
|
localStorage.setItem('tokenExpiry', String(Date.now() + response.data.expiresIn * 1000));
|
||||||
|
|
||||||
|
// Redirect to admin dashboard
|
||||||
|
router.push('/admin/dashboard');
|
||||||
|
} catch (err) {
|
||||||
|
setError('Neispravno korisničko ime ili lozinka');
|
||||||
|
console.error('Login error:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
||||||
|
Admin Prijava
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Prijavite se za upravljanje filamentima
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
|
||||||
|
<p className="text-sm text-red-800 dark:text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="username" className="sr-only">
|
||||||
|
Korisničko ime
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
autoComplete="username"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm dark:bg-gray-800"
|
||||||
|
placeholder="Korisničko ime"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="sr-only">
|
||||||
|
Lozinka
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm dark:bg-gray-800"
|
||||||
|
placeholder="Lozinka"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? 'Prijavljivanje...' : 'Prijavite se'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
app/page.tsx
59
app/page.tsx
@@ -10,31 +10,42 @@ export default function Home() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
const [darkMode, setDarkMode] = useState(() => {
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
if (typeof window !== 'undefined') {
|
const [mounted, setMounted] = useState(false);
|
||||||
const saved = localStorage.getItem('darkMode');
|
|
||||||
return saved ? JSON.parse(saved) : false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Initialize dark mode from localStorage after mounting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
setMounted(true);
|
||||||
localStorage.setItem('darkMode', JSON.stringify(darkMode));
|
const saved = localStorage.getItem('darkMode');
|
||||||
if (darkMode) {
|
if (saved) {
|
||||||
document.documentElement.classList.add('dark');
|
setDarkMode(JSON.parse(saved));
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [darkMode]);
|
}, []);
|
||||||
|
|
||||||
|
// Update dark mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
localStorage.setItem('darkMode', JSON.stringify(darkMode));
|
||||||
|
if (darkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}, [darkMode, mounted]);
|
||||||
|
|
||||||
const fetchFilaments = async () => {
|
const fetchFilaments = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await axios.get('/data.json');
|
// Use API if available, fallback to static JSON
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
const url = apiUrl ? `${apiUrl}/filaments` : '/data.json';
|
||||||
|
console.log('Fetching from:', url);
|
||||||
|
console.log('API URL configured:', apiUrl);
|
||||||
|
const response = await axios.get(url);
|
||||||
|
console.log('Response data:', response.data);
|
||||||
setFilaments(response.data);
|
setFilaments(response.data);
|
||||||
setLastUpdate(new Date());
|
setLastUpdate(new Date());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -74,13 +85,15 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
{loading ? 'Ažuriranje...' : 'Osveži'}
|
{loading ? 'Ažuriranje...' : 'Osveži'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
{mounted && (
|
||||||
onClick={() => setDarkMode(!darkMode)}
|
<button
|
||||||
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"
|
onClick={() => setDarkMode(!darkMode)}
|
||||||
title={darkMode ? 'Svetla tema' : 'Tamna tema'}
|
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>
|
{darkMode ? '☀️' : '🌙'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
lambda/auth.zip
Normal file
BIN
lambda/auth.zip
Normal file
Binary file not shown.
110
lambda/auth/index.js
Normal file
110
lambda/auth/index.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
const ADMIN_USERNAME = process.env.ADMIN_USERNAME;
|
||||||
|
const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH;
|
||||||
|
|
||||||
|
// CORS headers
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': process.env.CORS_ORIGIN || '*',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
|
||||||
|
'Access-Control-Allow-Methods': 'POST,OPTIONS'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to create response
|
||||||
|
const createResponse = (statusCode, body) => ({
|
||||||
|
statusCode,
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login handler
|
||||||
|
const login = async (event) => {
|
||||||
|
try {
|
||||||
|
const { username, password } = JSON.parse(event.body);
|
||||||
|
|
||||||
|
// Validate credentials
|
||||||
|
if (username !== ADMIN_USERNAME) {
|
||||||
|
return createResponse(401, { error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare password with hash
|
||||||
|
const isValid = await bcrypt.compare(password, ADMIN_PASSWORD_HASH);
|
||||||
|
if (!isValid) {
|
||||||
|
return createResponse(401, { error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ username, role: 'admin' },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: '24h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return createResponse(200, {
|
||||||
|
token,
|
||||||
|
expiresIn: 86400 // 24 hours in seconds
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
return createResponse(500, { error: 'Authentication failed' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify token (for Lambda authorizer)
|
||||||
|
const verifyToken = async (event) => {
|
||||||
|
try {
|
||||||
|
const token = event.authorizationToken?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET);
|
||||||
|
|
||||||
|
return {
|
||||||
|
principalId: decoded.username,
|
||||||
|
policyDocument: {
|
||||||
|
Version: '2012-10-17',
|
||||||
|
Statement: [
|
||||||
|
{
|
||||||
|
Action: 'execute-api:Invoke',
|
||||||
|
Effect: 'Allow',
|
||||||
|
Resource: event.methodArn
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
username: decoded.username,
|
||||||
|
role: decoded.role
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token verification error:', error);
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main handler
|
||||||
|
exports.handler = async (event) => {
|
||||||
|
const { httpMethod, resource } = event;
|
||||||
|
|
||||||
|
// Handle CORS preflight
|
||||||
|
if (httpMethod === 'OPTIONS') {
|
||||||
|
return createResponse(200, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle login
|
||||||
|
if (resource === '/auth/login' && httpMethod === 'POST') {
|
||||||
|
return login(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle token verification (for Lambda authorizer)
|
||||||
|
if (event.type === 'TOKEN') {
|
||||||
|
return verifyToken(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResponse(404, { error: 'Not found' });
|
||||||
|
};
|
||||||
161
lambda/auth/package-lock.json
generated
Normal file
161
lambda/auth/package-lock.json
generated
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"name": "auth-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "auth-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"jsonwebtoken": "^9.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^3.2.2",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^1.4.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lambda/auth/package.json
Normal file
17
lambda/auth/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "auth-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Lambda function for authentication",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"jsonwebtoken": "^9.0.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
BIN
lambda/filaments.zip
Normal file
BIN
lambda/filaments.zip
Normal file
Binary file not shown.
232
lambda/filaments/index.js
Normal file
232
lambda/filaments/index.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
const AWS = require('aws-sdk');
|
||||||
|
const dynamodb = new AWS.DynamoDB.DocumentClient();
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
const TABLE_NAME = process.env.TABLE_NAME;
|
||||||
|
|
||||||
|
// CORS headers
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': process.env.CORS_ORIGIN || '*',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
|
||||||
|
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to create response
|
||||||
|
const createResponse = (statusCode, body) => ({
|
||||||
|
statusCode,
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET all filaments or filter by query params
|
||||||
|
const getFilaments = async (event) => {
|
||||||
|
try {
|
||||||
|
const queryParams = event.queryStringParameters || {};
|
||||||
|
|
||||||
|
let params = {
|
||||||
|
TableName: TABLE_NAME
|
||||||
|
};
|
||||||
|
|
||||||
|
// If filtering by brand, type, or status, use the appropriate index
|
||||||
|
if (queryParams.brand) {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
IndexName: 'brand-index',
|
||||||
|
KeyConditionExpression: 'brand = :brand',
|
||||||
|
ExpressionAttributeValues: {
|
||||||
|
':brand': queryParams.brand
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = await dynamodb.query(params).promise();
|
||||||
|
return createResponse(200, result.Items);
|
||||||
|
} else if (queryParams.tip) {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
IndexName: 'tip-index',
|
||||||
|
KeyConditionExpression: 'tip = :tip',
|
||||||
|
ExpressionAttributeValues: {
|
||||||
|
':tip': queryParams.tip
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = await dynamodb.query(params).promise();
|
||||||
|
return createResponse(200, result.Items);
|
||||||
|
} else if (queryParams.status) {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
IndexName: 'status-index',
|
||||||
|
KeyConditionExpression: 'status = :status',
|
||||||
|
ExpressionAttributeValues: {
|
||||||
|
':status': queryParams.status
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = await dynamodb.query(params).promise();
|
||||||
|
return createResponse(200, result.Items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all items
|
||||||
|
const result = await dynamodb.scan(params).promise();
|
||||||
|
return createResponse(200, result.Items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting filaments:', error);
|
||||||
|
return createResponse(500, { error: 'Failed to fetch filaments' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET single filament by ID
|
||||||
|
const getFilament = async (event) => {
|
||||||
|
try {
|
||||||
|
const { id } = event.pathParameters;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
Key: { id }
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await dynamodb.get(params).promise();
|
||||||
|
|
||||||
|
if (!result.Item) {
|
||||||
|
return createResponse(404, { error: 'Filament not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResponse(200, result.Item);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting filament:', error);
|
||||||
|
return createResponse(500, { error: 'Failed to fetch filament' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// POST - Create new filament
|
||||||
|
const createFilament = async (event) => {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(event.body);
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Determine status based on vakum and otvoreno fields
|
||||||
|
let status = 'new';
|
||||||
|
if (body.otvoreno && body.otvoreno.toLowerCase().includes('otvorena')) {
|
||||||
|
status = 'opened';
|
||||||
|
} else if (body.refill && body.refill.toLowerCase() === 'da') {
|
||||||
|
status = 'refill';
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: uuidv4(),
|
||||||
|
...body,
|
||||||
|
status,
|
||||||
|
createdAt: timestamp,
|
||||||
|
updatedAt: timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
Item: item
|
||||||
|
};
|
||||||
|
|
||||||
|
await dynamodb.put(params).promise();
|
||||||
|
return createResponse(201, item);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating filament:', error);
|
||||||
|
return createResponse(500, { error: 'Failed to create filament' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// PUT - Update filament
|
||||||
|
const updateFilament = async (event) => {
|
||||||
|
try {
|
||||||
|
const { id } = event.pathParameters;
|
||||||
|
const body = JSON.parse(event.body);
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Determine status based on vakum and otvoreno fields
|
||||||
|
let status = 'new';
|
||||||
|
if (body.otvoreno && body.otvoreno.toLowerCase().includes('otvorena')) {
|
||||||
|
status = 'opened';
|
||||||
|
} else if (body.refill && body.refill.toLowerCase() === 'da') {
|
||||||
|
status = 'refill';
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
Key: { id },
|
||||||
|
UpdateExpression: `SET
|
||||||
|
brand = :brand,
|
||||||
|
tip = :tip,
|
||||||
|
finish = :finish,
|
||||||
|
boja = :boja,
|
||||||
|
refill = :refill,
|
||||||
|
vakum = :vakum,
|
||||||
|
otvoreno = :otvoreno,
|
||||||
|
kolicina = :kolicina,
|
||||||
|
cena = :cena,
|
||||||
|
#status = :status,
|
||||||
|
updatedAt = :updatedAt`,
|
||||||
|
ExpressionAttributeNames: {
|
||||||
|
'#status': 'status'
|
||||||
|
},
|
||||||
|
ExpressionAttributeValues: {
|
||||||
|
':brand': body.brand,
|
||||||
|
':tip': body.tip,
|
||||||
|
':finish': body.finish,
|
||||||
|
':boja': body.boja,
|
||||||
|
':refill': body.refill,
|
||||||
|
':vakum': body.vakum,
|
||||||
|
':otvoreno': body.otvoreno,
|
||||||
|
':kolicina': body.kolicina,
|
||||||
|
':cena': body.cena,
|
||||||
|
':status': status,
|
||||||
|
':updatedAt': timestamp
|
||||||
|
},
|
||||||
|
ReturnValues: 'ALL_NEW'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await dynamodb.update(params).promise();
|
||||||
|
return createResponse(200, result.Attributes);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating filament:', error);
|
||||||
|
return createResponse(500, { error: 'Failed to update filament' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DELETE filament
|
||||||
|
const deleteFilament = async (event) => {
|
||||||
|
try {
|
||||||
|
const { id } = event.pathParameters;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
Key: { id }
|
||||||
|
};
|
||||||
|
|
||||||
|
await dynamodb.delete(params).promise();
|
||||||
|
return createResponse(200, { message: 'Filament deleted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting filament:', error);
|
||||||
|
return createResponse(500, { error: 'Failed to delete filament' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main handler
|
||||||
|
exports.handler = async (event) => {
|
||||||
|
const { httpMethod, resource } = event;
|
||||||
|
|
||||||
|
// Handle CORS preflight
|
||||||
|
if (httpMethod === 'OPTIONS') {
|
||||||
|
return createResponse(200, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route requests
|
||||||
|
if (resource === '/filaments' && httpMethod === 'GET') {
|
||||||
|
return getFilaments(event);
|
||||||
|
} else if (resource === '/filaments' && httpMethod === 'POST') {
|
||||||
|
return createFilament(event);
|
||||||
|
} else if (resource === '/filaments/{id}' && httpMethod === 'GET') {
|
||||||
|
return getFilament(event);
|
||||||
|
} else if (resource === '/filaments/{id}' && httpMethod === 'PUT') {
|
||||||
|
return updateFilament(event);
|
||||||
|
} else if (resource === '/filaments/{id}' && httpMethod === 'DELETE') {
|
||||||
|
return deleteFilament(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResponse(404, { error: 'Not found' });
|
||||||
|
};
|
||||||
593
lambda/filaments/package-lock.json
generated
Normal file
593
lambda/filaments/package-lock.json
generated
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
{
|
||||||
|
"name": "filaments-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "filaments-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.1692.0",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/available-typed-arrays": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"possible-typed-array-names": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-sdk": {
|
||||||
|
"version": "2.1692.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz",
|
||||||
|
"integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "4.9.2",
|
||||||
|
"events": "1.1.1",
|
||||||
|
"ieee754": "1.1.13",
|
||||||
|
"jmespath": "0.16.0",
|
||||||
|
"querystring": "0.2.0",
|
||||||
|
"sax": "1.2.1",
|
||||||
|
"url": "0.10.3",
|
||||||
|
"util": "^0.12.4",
|
||||||
|
"uuid": "8.0.0",
|
||||||
|
"xml2js": "0.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aws-sdk/node_modules/uuid": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "4.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
|
||||||
|
"integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.0.2",
|
||||||
|
"ieee754": "^1.1.4",
|
||||||
|
"isarray": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.0",
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"set-function-length": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/for-each": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-callable": "^1.2.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||||
|
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-callable": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-generator-function": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.3",
|
||||||
|
"get-proto": "^1.0.0",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"safe-regex-test": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-regex": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-typed-array": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"which-typed-array": "^1.1.16"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jmespath": {
|
||||||
|
"version": "0.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
|
||||||
|
"integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/possible-typed-array-names": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/querystring": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
|
||||||
|
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safe-regex-test": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"is-regex": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/set-function-length": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.1.4",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"gopd": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/url": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
|
||||||
|
"integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "1.3.2",
|
||||||
|
"querystring": "0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/which-typed-array": {
|
||||||
|
"version": "1.1.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||||
|
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"available-typed-arrays": "^1.0.7",
|
||||||
|
"call-bind": "^1.0.8",
|
||||||
|
"call-bound": "^1.0.4",
|
||||||
|
"for-each": "^0.3.5",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml2js": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sax": ">=0.6.0",
|
||||||
|
"xmlbuilder": "~11.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlbuilder": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lambda/filaments/package.json
Normal file
17
lambda/filaments/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "filaments-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Lambda function for filaments CRUD operations",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.1692.0",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cheerio": "^0.22.35",
|
"@types/cheerio": "^0.22.35",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"cheerio": "^1.1.0",
|
"cheerio": "^1.1.0",
|
||||||
"next": "^15.3.4",
|
"next": "^15.3.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
@@ -4214,6 +4215,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
|||||||
@@ -11,11 +11,14 @@
|
|||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"security:check": "node scripts/security-check.js",
|
"security:check": "node scripts/security-check.js",
|
||||||
"test:build": "node scripts/test-build.js",
|
"test:build": "node scripts/test-build.js",
|
||||||
"prepare": "husky"
|
"prepare": "husky",
|
||||||
|
"migrate": "cd scripts && npm install && npm run migrate",
|
||||||
|
"migrate:clear": "cd scripts && npm install && npm run migrate:clear"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cheerio": "^0.22.35",
|
"@types/cheerio": "^0.22.35",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"cheerio": "^1.1.0",
|
"cheerio": "^1.1.0",
|
||||||
"next": "^15.3.4",
|
"next": "^15.3.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
|||||||
167
public/data.json
Normal file
167
public/data.json
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"brand": "Bambu Lab",
|
||||||
|
"tip": "PLA",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Lavender Purple",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "2500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Bambu Lab",
|
||||||
|
"tip": "PLA",
|
||||||
|
"finish": "Matte",
|
||||||
|
"boja": "Charcoal Black",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "",
|
||||||
|
"otvoreno": "otvorena",
|
||||||
|
"kolicina": "0.8kg",
|
||||||
|
"cena": "2800"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Bambu Lab",
|
||||||
|
"tip": "PETG",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Transparent",
|
||||||
|
"refill": "Da",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "3200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Azure Film",
|
||||||
|
"tip": "PLA",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "White",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "2200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Azure Film",
|
||||||
|
"tip": "PETG",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Orange",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "",
|
||||||
|
"otvoreno": "otvorena",
|
||||||
|
"kolicina": "0.5kg",
|
||||||
|
"cena": "2600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Bambu Lab",
|
||||||
|
"tip": "Silk PLA",
|
||||||
|
"finish": "Silk",
|
||||||
|
"boja": "Gold",
|
||||||
|
"refill": "Da",
|
||||||
|
"vakum": "",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "0.5kg",
|
||||||
|
"cena": "3500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Bambu Lab",
|
||||||
|
"tip": "PLA Matte",
|
||||||
|
"finish": "Matte",
|
||||||
|
"boja": "Forest Green",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "2800"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "PanaChroma",
|
||||||
|
"tip": "PLA",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Red",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "2300"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "PanaChroma",
|
||||||
|
"tip": "PETG",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Blue",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "",
|
||||||
|
"otvoreno": "otvorena",
|
||||||
|
"kolicina": "0.75kg",
|
||||||
|
"cena": "2700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Fiberlogy",
|
||||||
|
"tip": "PLA",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Gray",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "0.85kg",
|
||||||
|
"cena": "2400"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Fiberlogy",
|
||||||
|
"tip": "ABS",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Black",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "2900"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Fiberlogy",
|
||||||
|
"tip": "TPU",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Lime Green",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "",
|
||||||
|
"otvoreno": "otvorena",
|
||||||
|
"kolicina": "0.3kg",
|
||||||
|
"cena": "4500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Azure Film",
|
||||||
|
"tip": "PLA",
|
||||||
|
"finish": "Silk",
|
||||||
|
"boja": "Silver",
|
||||||
|
"refill": "Da",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "2800"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "Bambu Lab",
|
||||||
|
"tip": "PLA",
|
||||||
|
"finish": "Basic",
|
||||||
|
"boja": "Jade White",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "",
|
||||||
|
"otvoreno": "otvorena",
|
||||||
|
"kolicina": "0.5kg",
|
||||||
|
"cena": "2500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"brand": "PanaChroma",
|
||||||
|
"tip": "Silk PLA",
|
||||||
|
"finish": "Silk",
|
||||||
|
"boja": "Copper",
|
||||||
|
"refill": "",
|
||||||
|
"vakum": "u vakuumu",
|
||||||
|
"otvoreno": "",
|
||||||
|
"kolicina": "1kg",
|
||||||
|
"cena": "3200"
|
||||||
|
}
|
||||||
|
]
|
||||||
84
scripts/README.md
Normal file
84
scripts/README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Data Migration Scripts
|
||||||
|
|
||||||
|
This directory contains scripts for migrating filament data from Confluence to DynamoDB.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. AWS credentials configured (either via AWS CLI or environment variables)
|
||||||
|
2. DynamoDB table created via Terraform
|
||||||
|
3. Confluence API credentials (if migrating from Confluence)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env.local` file in the project root with:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# AWS Configuration
|
||||||
|
AWS_REGION=eu-central-1
|
||||||
|
DYNAMODB_TABLE_NAME=filamenteka-filaments
|
||||||
|
|
||||||
|
# Confluence Configuration (optional)
|
||||||
|
CONFLUENCE_API_URL=https://your-domain.atlassian.net
|
||||||
|
CONFLUENCE_TOKEN=your-email:your-api-token
|
||||||
|
CONFLUENCE_PAGE_ID=your-page-id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Migrate from local data (data.json)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear existing data and migrate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run migrate:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual execution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migrate without clearing
|
||||||
|
node migrate-with-parser.js
|
||||||
|
|
||||||
|
# Clear existing data first
|
||||||
|
node migrate-with-parser.js --clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## What the script does
|
||||||
|
|
||||||
|
1. **Checks for Confluence credentials**
|
||||||
|
- If found: Fetches data from Confluence page
|
||||||
|
- If not found: Uses local `public/data.json` file
|
||||||
|
|
||||||
|
2. **Parses the data**
|
||||||
|
- Extracts filament information from HTML table (Confluence)
|
||||||
|
- Or reads JSON directly (local file)
|
||||||
|
|
||||||
|
3. **Prepares data for DynamoDB**
|
||||||
|
- Generates unique IDs for each filament
|
||||||
|
- Adds timestamps (createdAt, updatedAt)
|
||||||
|
|
||||||
|
4. **Writes to DynamoDB**
|
||||||
|
- Writes in batches of 25 items (DynamoDB limit)
|
||||||
|
- Shows progress during migration
|
||||||
|
|
||||||
|
5. **Verifies the migration**
|
||||||
|
- Counts total items in DynamoDB
|
||||||
|
- Shows a sample item for verification
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Table not found**: Make sure you've run `terraform apply` first
|
||||||
|
- **Access denied**: Check your AWS credentials and permissions
|
||||||
|
- **Confluence errors**: Verify your API token and page ID
|
||||||
|
- **Empty migration**: Check that the Confluence page has a table with the expected format
|
||||||
13
scripts/generate-password-hash.js
Normal file
13
scripts/generate-password-hash.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
const password = process.argv[2];
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
console.error('Usage: node generate-password-hash.js <password>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = bcrypt.hashSync(password, 10);
|
||||||
|
console.log('Password hash:', hash);
|
||||||
|
console.log('\nAdd this to your terraform.tfvars:');
|
||||||
|
console.log(`admin_password_hash = "${hash}"`);
|
||||||
194
scripts/migrate-confluence-to-dynamo.js
Normal file
194
scripts/migrate-confluence-to-dynamo.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('dotenv').config({ path: '.env.local' });
|
||||||
|
const axios = require('axios');
|
||||||
|
const AWS = require('aws-sdk');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
// Configure AWS
|
||||||
|
AWS.config.update({
|
||||||
|
region: process.env.AWS_REGION || 'eu-central-1'
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamodb = new AWS.DynamoDB.DocumentClient();
|
||||||
|
const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'filamenteka-filaments';
|
||||||
|
|
||||||
|
// Confluence configuration
|
||||||
|
const CONFLUENCE_API_URL = process.env.CONFLUENCE_API_URL;
|
||||||
|
const CONFLUENCE_TOKEN = process.env.CONFLUENCE_TOKEN;
|
||||||
|
const CONFLUENCE_PAGE_ID = process.env.CONFLUENCE_PAGE_ID;
|
||||||
|
|
||||||
|
async function fetchConfluenceData() {
|
||||||
|
try {
|
||||||
|
console.log('Fetching data from Confluence...');
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFLUENCE_API_URL}/wiki/rest/api/content/${CONFLUENCE_PAGE_ID}?expand=body.storage`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${Buffer.from(CONFLUENCE_TOKEN).toString('base64')}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const htmlContent = response.data.body.storage.value;
|
||||||
|
return parseConfluenceTable(htmlContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching from Confluence:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfluenceTable(html) {
|
||||||
|
// Simple HTML table parser - in production, use a proper HTML parser like cheerio
|
||||||
|
const rows = [];
|
||||||
|
const tableRegex = /<tr[^>]*>(.*?)<\/tr>/gs;
|
||||||
|
const cellRegex = /<t[dh][^>]*>(.*?)<\/t[dh]>/gs;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
let isHeader = true;
|
||||||
|
|
||||||
|
while ((match = tableRegex.exec(html)) !== null) {
|
||||||
|
const rowHtml = match[1];
|
||||||
|
const cells = [];
|
||||||
|
let cellMatch;
|
||||||
|
|
||||||
|
while ((cellMatch = cellRegex.exec(rowHtml)) !== null) {
|
||||||
|
// Remove HTML tags from cell content
|
||||||
|
const cellContent = cellMatch[1]
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.trim();
|
||||||
|
cells.push(cellContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHeader && cells.length > 0) {
|
||||||
|
rows.push(cells);
|
||||||
|
}
|
||||||
|
isHeader = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map rows to filament objects
|
||||||
|
return rows.map(row => ({
|
||||||
|
brand: row[0] || '',
|
||||||
|
tip: row[1] || '',
|
||||||
|
finish: row[2] || '',
|
||||||
|
boja: row[3] || '',
|
||||||
|
refill: row[4] || '',
|
||||||
|
vakum: row[5] || '',
|
||||||
|
otvoreno: row[6] || '',
|
||||||
|
kolicina: row[7] || '',
|
||||||
|
cena: row[8] || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateToLocalJSON() {
|
||||||
|
try {
|
||||||
|
console.log('Migrating to local JSON file for testing...');
|
||||||
|
|
||||||
|
// For now, use the mock data we created
|
||||||
|
const fs = require('fs');
|
||||||
|
const data = JSON.parse(fs.readFileSync('./public/data.json', 'utf8'));
|
||||||
|
|
||||||
|
const filaments = data.map(item => ({
|
||||||
|
id: uuidv4(),
|
||||||
|
...item,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`Found ${filaments.length} filaments to migrate`);
|
||||||
|
return filaments;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading local data:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateToDynamoDB(filaments) {
|
||||||
|
console.log(`Migrating ${filaments.length} filaments to DynamoDB...`);
|
||||||
|
|
||||||
|
// Check if table exists
|
||||||
|
try {
|
||||||
|
const dynamo = new AWS.DynamoDB();
|
||||||
|
await dynamo.describeTable({ TableName: TABLE_NAME }).promise();
|
||||||
|
console.log(`Table ${TABLE_NAME} exists`);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ResourceNotFoundException') {
|
||||||
|
console.error(`Table ${TABLE_NAME} does not exist. Please run Terraform first.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch write items
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < filaments.length; i += 25) {
|
||||||
|
chunks.push(filaments.slice(i, i + 25));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const params = {
|
||||||
|
RequestItems: {
|
||||||
|
[TABLE_NAME]: chunk.map(item => ({
|
||||||
|
PutRequest: { Item: item }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dynamodb.batchWrite(params).promise();
|
||||||
|
console.log(`Migrated ${chunk.length} items`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing batch:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Migration completed successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
let filaments;
|
||||||
|
|
||||||
|
if (CONFLUENCE_API_URL && CONFLUENCE_TOKEN && CONFLUENCE_PAGE_ID) {
|
||||||
|
// Fetch from Confluence
|
||||||
|
const confluenceData = await fetchConfluenceData();
|
||||||
|
filaments = confluenceData.map(item => ({
|
||||||
|
id: uuidv4(),
|
||||||
|
...item,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
console.log('Confluence credentials not found, using local data...');
|
||||||
|
filaments = await migrateToLocalJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate to DynamoDB
|
||||||
|
await migrateToDynamoDB(filaments);
|
||||||
|
|
||||||
|
// Verify migration
|
||||||
|
const params = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
Select: 'COUNT'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await dynamodb.scan(params).promise();
|
||||||
|
console.log(`\nVerification: ${result.Count} items in DynamoDB`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Migration failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
241
scripts/migrate-with-parser.js
Normal file
241
scripts/migrate-with-parser.js
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('dotenv').config({ path: '.env.local' });
|
||||||
|
const axios = require('axios');
|
||||||
|
const AWS = require('aws-sdk');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const cheerio = require('cheerio');
|
||||||
|
|
||||||
|
// Configure AWS
|
||||||
|
AWS.config.update({
|
||||||
|
region: process.env.AWS_REGION || 'eu-central-1'
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamodb = new AWS.DynamoDB.DocumentClient();
|
||||||
|
const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'filamenteka-filaments';
|
||||||
|
|
||||||
|
// Confluence configuration
|
||||||
|
const CONFLUENCE_API_URL = process.env.CONFLUENCE_API_URL;
|
||||||
|
const CONFLUENCE_TOKEN = process.env.CONFLUENCE_TOKEN;
|
||||||
|
const CONFLUENCE_PAGE_ID = process.env.CONFLUENCE_PAGE_ID;
|
||||||
|
|
||||||
|
async function fetchConfluenceData() {
|
||||||
|
try {
|
||||||
|
console.log('Fetching data from Confluence...');
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFLUENCE_API_URL}/wiki/rest/api/content/${CONFLUENCE_PAGE_ID}?expand=body.storage`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${Buffer.from(CONFLUENCE_TOKEN).toString('base64')}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const htmlContent = response.data.body.storage.value;
|
||||||
|
return parseConfluenceTable(htmlContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching from Confluence:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfluenceTable(html) {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const filaments = [];
|
||||||
|
|
||||||
|
// Find the table and iterate through rows
|
||||||
|
$('table').find('tr').each((index, row) => {
|
||||||
|
// Skip header row
|
||||||
|
if (index === 0) return;
|
||||||
|
|
||||||
|
const cells = $(row).find('td');
|
||||||
|
if (cells.length >= 9) {
|
||||||
|
const filament = {
|
||||||
|
brand: $(cells[0]).text().trim(),
|
||||||
|
tip: $(cells[1]).text().trim(),
|
||||||
|
finish: $(cells[2]).text().trim(),
|
||||||
|
boja: $(cells[3]).text().trim(),
|
||||||
|
refill: $(cells[4]).text().trim(),
|
||||||
|
vakum: $(cells[5]).text().trim(),
|
||||||
|
otvoreno: $(cells[6]).text().trim(),
|
||||||
|
kolicina: $(cells[7]).text().trim(),
|
||||||
|
cena: $(cells[8]).text().trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add if row has valid data
|
||||||
|
if (filament.brand || filament.boja) {
|
||||||
|
filaments.push(filament);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filaments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearDynamoTable() {
|
||||||
|
console.log('Clearing existing data from DynamoDB...');
|
||||||
|
|
||||||
|
// Scan all items
|
||||||
|
const scanParams = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
ProjectionExpression: 'id'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scanResult = await dynamodb.scan(scanParams).promise();
|
||||||
|
|
||||||
|
if (scanResult.Items.length === 0) {
|
||||||
|
console.log('Table is already empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete in batches
|
||||||
|
const deleteRequests = scanResult.Items.map(item => ({
|
||||||
|
DeleteRequest: { Key: { id: item.id } }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// DynamoDB batchWrite supports max 25 items
|
||||||
|
for (let i = 0; i < deleteRequests.length; i += 25) {
|
||||||
|
const batch = deleteRequests.slice(i, i + 25);
|
||||||
|
const params = {
|
||||||
|
RequestItems: {
|
||||||
|
[TABLE_NAME]: batch
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await dynamodb.batchWrite(params).promise();
|
||||||
|
console.log(`Deleted ${batch.length} items`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Table cleared successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing table:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateToDynamoDB(filaments) {
|
||||||
|
console.log(`Migrating ${filaments.length} filaments to DynamoDB...`);
|
||||||
|
|
||||||
|
// Check if table exists
|
||||||
|
try {
|
||||||
|
const dynamo = new AWS.DynamoDB();
|
||||||
|
await dynamo.describeTable({ TableName: TABLE_NAME }).promise();
|
||||||
|
console.log(`Table ${TABLE_NAME} exists`);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ResourceNotFoundException') {
|
||||||
|
console.error(`Table ${TABLE_NAME} does not exist. Please run Terraform first.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add IDs and timestamps
|
||||||
|
const itemsToInsert = filaments.map(item => ({
|
||||||
|
id: uuidv4(),
|
||||||
|
...item,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Batch write items (max 25 per batch)
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < itemsToInsert.length; i += 25) {
|
||||||
|
chunks.push(itemsToInsert.slice(i, i + 25));
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalMigrated = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const params = {
|
||||||
|
RequestItems: {
|
||||||
|
[TABLE_NAME]: chunk.map(item => ({
|
||||||
|
PutRequest: { Item: item }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dynamodb.batchWrite(params).promise();
|
||||||
|
totalMigrated += chunk.length;
|
||||||
|
console.log(`Migrated ${totalMigrated}/${itemsToInsert.length} items`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing batch:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Migration completed successfully!');
|
||||||
|
return totalMigrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
let filaments;
|
||||||
|
|
||||||
|
// Check for --clear flag
|
||||||
|
const shouldClear = process.argv.includes('--clear');
|
||||||
|
|
||||||
|
if (shouldClear) {
|
||||||
|
await clearDynamoTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CONFLUENCE_API_URL && CONFLUENCE_TOKEN && CONFLUENCE_PAGE_ID) {
|
||||||
|
// Fetch from Confluence
|
||||||
|
console.log('Using Confluence as data source');
|
||||||
|
filaments = await fetchConfluenceData();
|
||||||
|
} else {
|
||||||
|
console.log('Confluence credentials not found, using local mock data...');
|
||||||
|
const fs = require('fs');
|
||||||
|
const data = JSON.parse(fs.readFileSync('../public/data.json', 'utf8'));
|
||||||
|
filaments = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${filaments.length} filaments to migrate`);
|
||||||
|
|
||||||
|
// Show sample data
|
||||||
|
if (filaments.length > 0) {
|
||||||
|
console.log('\nSample data:');
|
||||||
|
console.log(JSON.stringify(filaments[0], null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate to DynamoDB
|
||||||
|
const migrated = await migrateToDynamoDB(filaments);
|
||||||
|
|
||||||
|
// Verify migration
|
||||||
|
const params = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
Select: 'COUNT'
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await dynamodb.scan(params).promise();
|
||||||
|
console.log(`\nVerification: ${result.Count} total items now in DynamoDB`);
|
||||||
|
|
||||||
|
// Show sample from DynamoDB
|
||||||
|
const sampleParams = {
|
||||||
|
TableName: TABLE_NAME,
|
||||||
|
Limit: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleResult = await dynamodb.scan(sampleParams).promise();
|
||||||
|
if (sampleResult.Items.length > 0) {
|
||||||
|
console.log('\nSample from DynamoDB:');
|
||||||
|
console.log(JSON.stringify(sampleResult.Items[0], null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Migration failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
if (require.main === module) {
|
||||||
|
console.log('Confluence to DynamoDB Migration Tool');
|
||||||
|
console.log('=====================================');
|
||||||
|
console.log('Usage: node migrate-with-parser.js [--clear]');
|
||||||
|
console.log(' --clear: Clear existing data before migration\n');
|
||||||
|
|
||||||
|
main();
|
||||||
|
}
|
||||||
1019
scripts/package-lock.json
generated
Normal file
1019
scripts/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
scripts/package.json
Normal file
16
scripts/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "filamenteka-scripts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Migration and utility scripts for Filamenteka",
|
||||||
|
"scripts": {
|
||||||
|
"migrate": "node migrate-with-parser.js",
|
||||||
|
"migrate:clear": "node migrate-with-parser.js --clear"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.1472.0",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
|
|||||||
import { Filament } from '../types/filament';
|
import { Filament } from '../types/filament';
|
||||||
import { ColorCell } from './ColorCell';
|
import { ColorCell } from './ColorCell';
|
||||||
import { getFilamentColor, getColorStyle, getContrastColor } from '../data/bambuLabColors';
|
import { getFilamentColor, getColorStyle, getContrastColor } from '../data/bambuLabColors';
|
||||||
|
import '../styles/select.css';
|
||||||
|
|
||||||
interface FilamentTableProps {
|
interface FilamentTableProps {
|
||||||
filaments: Filament[];
|
filaments: Filament[];
|
||||||
@@ -13,13 +14,48 @@ export const FilamentTable: React.FC<FilamentTableProps> = ({ filaments, loading
|
|||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortField, setSortField] = useState<keyof Filament>('boja');
|
const [sortField, setSortField] = useState<keyof Filament>('boja');
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
||||||
|
|
||||||
|
// Filter states
|
||||||
|
const [filterBrand, setFilterBrand] = useState('');
|
||||||
|
const [filterTip, setFilterTip] = useState('');
|
||||||
|
const [filterFinish, setFilterFinish] = useState('');
|
||||||
|
const [filterStatus, setFilterStatus] = useState('');
|
||||||
|
|
||||||
|
// Get unique values for filters
|
||||||
|
const uniqueBrands = useMemo(() => [...new Set(filaments.map(f => f.brand))].sort(), [filaments]);
|
||||||
|
const uniqueTips = useMemo(() => [...new Set(filaments.map(f => f.tip))].sort(), [filaments]);
|
||||||
|
const uniqueFinishes = useMemo(() => [...new Set(filaments.map(f => f.finish))].sort(), [filaments]);
|
||||||
|
|
||||||
const filteredAndSortedFilaments = useMemo(() => {
|
const filteredAndSortedFilaments = useMemo(() => {
|
||||||
let filtered = filaments.filter(filament =>
|
let filtered = filaments.filter(filament => {
|
||||||
Object.values(filament).some(value =>
|
// Search filter
|
||||||
|
const matchesSearch = Object.values(filament).some(value =>
|
||||||
value.toLowerCase().includes(searchTerm.toLowerCase())
|
value.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
);
|
||||||
);
|
|
||||||
|
// Brand filter
|
||||||
|
const matchesBrand = !filterBrand || filament.brand === filterBrand;
|
||||||
|
|
||||||
|
// Type filter
|
||||||
|
const matchesTip = !filterTip || filament.tip === filterTip;
|
||||||
|
|
||||||
|
// Finish filter
|
||||||
|
const matchesFinish = !filterFinish || filament.finish === filterFinish;
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
let matchesStatus = true;
|
||||||
|
if (filterStatus) {
|
||||||
|
if (filterStatus === 'new') {
|
||||||
|
matchesStatus = filament.vakum.toLowerCase().includes('vakuum') && !filament.otvoreno;
|
||||||
|
} else if (filterStatus === 'opened') {
|
||||||
|
matchesStatus = filament.otvoreno.toLowerCase().includes('otvorena');
|
||||||
|
} else if (filterStatus === 'refill') {
|
||||||
|
matchesStatus = filament.refill.toLowerCase() === 'da';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchesSearch && matchesBrand && matchesTip && matchesFinish && matchesStatus;
|
||||||
|
});
|
||||||
|
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
const aValue = a[sortField];
|
const aValue = a[sortField];
|
||||||
@@ -33,7 +69,7 @@ export const FilamentTable: React.FC<FilamentTableProps> = ({ filaments, loading
|
|||||||
});
|
});
|
||||||
|
|
||||||
return filtered;
|
return filtered;
|
||||||
}, [filaments, searchTerm, sortField, sortOrder]);
|
}, [filaments, searchTerm, sortField, sortOrder, filterBrand, filterTip, filterFinish, filterStatus]);
|
||||||
|
|
||||||
const handleSort = (field: keyof Filament) => {
|
const handleSort = (field: keyof Filament) => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
@@ -62,14 +98,103 @@ export const FilamentTable: React.FC<FilamentTableProps> = ({ filaments, loading
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mb-4">
|
<div className="space-y-4 mb-4">
|
||||||
<input
|
{/* Search Bar */}
|
||||||
type="text"
|
<div>
|
||||||
placeholder="Pretraži filamente..."
|
<input
|
||||||
value={searchTerm}
|
type="text"
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
placeholder="Pretraži filamente..."
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
value={searchTerm}
|
||||||
/>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Row */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{/* Brand Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Brend
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filterBrand}
|
||||||
|
onChange={(e) => setFilterBrand(e.target.value)}
|
||||||
|
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Svi brendovi</option>
|
||||||
|
{uniqueBrands.map(brand => (
|
||||||
|
<option key={brand} value={brand}>{brand}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Tip
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filterTip}
|
||||||
|
onChange={(e) => setFilterTip(e.target.value)}
|
||||||
|
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Svi tipovi</option>
|
||||||
|
{uniqueTips.map(tip => (
|
||||||
|
<option key={tip} value={tip}>{tip}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Finish Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Završna obrada
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filterFinish}
|
||||||
|
onChange={(e) => setFilterFinish(e.target.value)}
|
||||||
|
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Sve obrade</option>
|
||||||
|
{uniqueFinishes.map(finish => (
|
||||||
|
<option key={finish} value={finish}>{finish}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
|
className="custom-select w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Svi statusi</option>
|
||||||
|
<option value="new">Novi (vakuum)</option>
|
||||||
|
<option value="opened">Otvoreni</option>
|
||||||
|
<option value="refill">Refill</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Clear Filters Button */}
|
||||||
|
{(filterBrand || filterTip || filterFinish || filterStatus) && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setFilterBrand('');
|
||||||
|
setFilterTip('');
|
||||||
|
setFilterFinish('');
|
||||||
|
setFilterStatus('');
|
||||||
|
}}
|
||||||
|
className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
Obriši filtere
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|||||||
24
src/styles/select.css
Normal file
24
src/styles/select.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* Custom select styling for cross-browser consistency */
|
||||||
|
.custom-select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
background-size: 1.5em 1.5em;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-select {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safari-specific fixes */
|
||||||
|
@media not all and (min-resolution:.001dpcm) {
|
||||||
|
@supports (-webkit-appearance:none) {
|
||||||
|
.custom-select {
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
281
terraform/api_gateway.tf
Normal file
281
terraform/api_gateway.tf
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# API Gateway REST API
|
||||||
|
resource "aws_api_gateway_rest_api" "api" {
|
||||||
|
name = "${var.app_name}-api"
|
||||||
|
description = "API for ${var.app_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Gateway Resources
|
||||||
|
resource "aws_api_gateway_resource" "filaments" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
parent_id = aws_api_gateway_rest_api.api.root_resource_id
|
||||||
|
path_part = "filaments"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_resource" "filament" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
parent_id = aws_api_gateway_resource.filaments.id
|
||||||
|
path_part = "{id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_resource" "auth" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
parent_id = aws_api_gateway_rest_api.api.root_resource_id
|
||||||
|
path_part = "auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_resource" "login" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
parent_id = aws_api_gateway_resource.auth.id
|
||||||
|
path_part = "login"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lambda Authorizer
|
||||||
|
resource "aws_api_gateway_authorizer" "jwt_authorizer" {
|
||||||
|
name = "${var.app_name}-jwt-authorizer"
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
type = "TOKEN"
|
||||||
|
authorizer_uri = aws_lambda_function.auth_api.invoke_arn
|
||||||
|
authorizer_credentials = aws_iam_role.api_gateway_auth_invocation.arn
|
||||||
|
identity_source = "method.request.header.Authorization"
|
||||||
|
}
|
||||||
|
|
||||||
|
# IAM role for API Gateway to invoke Lambda authorizer
|
||||||
|
resource "aws_iam_role" "api_gateway_auth_invocation" {
|
||||||
|
name = "${var.app_name}-api-gateway-auth-invocation"
|
||||||
|
|
||||||
|
assume_role_policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Action = "sts:AssumeRole"
|
||||||
|
Effect = "Allow"
|
||||||
|
Principal = {
|
||||||
|
Service = "apigateway.amazonaws.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role_policy" "api_gateway_auth_invocation" {
|
||||||
|
name = "${var.app_name}-api-gateway-auth-invocation"
|
||||||
|
role = aws_iam_role.api_gateway_auth_invocation.id
|
||||||
|
|
||||||
|
policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = "lambda:InvokeFunction"
|
||||||
|
Resource = aws_lambda_function.auth_api.arn
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Methods for /filaments
|
||||||
|
resource "aws_api_gateway_method" "get_filaments" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filaments.id
|
||||||
|
http_method = "GET"
|
||||||
|
authorization = "NONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_method" "post_filament" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filaments.id
|
||||||
|
http_method = "POST"
|
||||||
|
authorization = "CUSTOM"
|
||||||
|
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Methods for /filaments/{id}
|
||||||
|
resource "aws_api_gateway_method" "get_filament" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = "GET"
|
||||||
|
authorization = "NONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_method" "put_filament" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = "PUT"
|
||||||
|
authorization = "CUSTOM"
|
||||||
|
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_method" "delete_filament" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = "DELETE"
|
||||||
|
authorization = "CUSTOM"
|
||||||
|
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Method for /auth/login
|
||||||
|
resource "aws_api_gateway_method" "login" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.login.id
|
||||||
|
http_method = "POST"
|
||||||
|
authorization = "NONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# OPTIONS methods for CORS
|
||||||
|
resource "aws_api_gateway_method" "options_filaments" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filaments.id
|
||||||
|
http_method = "OPTIONS"
|
||||||
|
authorization = "NONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_method" "options_filament" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = "OPTIONS"
|
||||||
|
authorization = "NONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_method" "options_login" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.login.id
|
||||||
|
http_method = "OPTIONS"
|
||||||
|
authorization = "NONE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lambda integrations
|
||||||
|
resource "aws_api_gateway_integration" "filaments_get" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filaments.id
|
||||||
|
http_method = aws_api_gateway_method.get_filaments.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_integration" "filaments_post" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filaments.id
|
||||||
|
http_method = aws_api_gateway_method.post_filament.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_integration" "filament_get" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = aws_api_gateway_method.get_filament.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_integration" "filament_put" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = aws_api_gateway_method.put_filament.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_integration" "filament_delete" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = aws_api_gateway_method.delete_filament.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_integration" "login" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.login.id
|
||||||
|
http_method = aws_api_gateway_method.login.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.auth_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
# OPTIONS integrations for CORS
|
||||||
|
resource "aws_api_gateway_integration" "options_filaments" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filaments.id
|
||||||
|
http_method = aws_api_gateway_method.options_filaments.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_integration" "options_filament" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.filament.id
|
||||||
|
http_method = aws_api_gateway_method.options_filament.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_api_gateway_integration" "options_login" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
resource_id = aws_api_gateway_resource.login.id
|
||||||
|
http_method = aws_api_gateway_method.options_login.http_method
|
||||||
|
|
||||||
|
integration_http_method = "POST"
|
||||||
|
type = "AWS_PROXY"
|
||||||
|
uri = aws_lambda_function.auth_api.invoke_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lambda permissions for API Gateway
|
||||||
|
resource "aws_lambda_permission" "api_gateway_filaments" {
|
||||||
|
statement_id = "AllowAPIGatewayInvoke"
|
||||||
|
action = "lambda:InvokeFunction"
|
||||||
|
function_name = aws_lambda_function.filaments_api.function_name
|
||||||
|
principal = "apigateway.amazonaws.com"
|
||||||
|
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_lambda_permission" "api_gateway_auth" {
|
||||||
|
statement_id = "AllowAPIGatewayInvoke"
|
||||||
|
action = "lambda:InvokeFunction"
|
||||||
|
function_name = aws_lambda_function.auth_api.function_name
|
||||||
|
principal = "apigateway.amazonaws.com"
|
||||||
|
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Gateway Deployment
|
||||||
|
resource "aws_api_gateway_deployment" "api" {
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
aws_api_gateway_integration.filaments_get,
|
||||||
|
aws_api_gateway_integration.filaments_post,
|
||||||
|
aws_api_gateway_integration.filament_get,
|
||||||
|
aws_api_gateway_integration.filament_put,
|
||||||
|
aws_api_gateway_integration.filament_delete,
|
||||||
|
aws_api_gateway_integration.login,
|
||||||
|
aws_api_gateway_integration.options_filaments,
|
||||||
|
aws_api_gateway_integration.options_filament,
|
||||||
|
aws_api_gateway_integration.options_login
|
||||||
|
]
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
create_before_destroy = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Gateway Stage
|
||||||
|
resource "aws_api_gateway_stage" "api" {
|
||||||
|
deployment_id = aws_api_gateway_deployment.api.id
|
||||||
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||||
|
stage_name = var.environment
|
||||||
|
}
|
||||||
22
terraform/cloudflare-dns.tf
Normal file
22
terraform/cloudflare-dns.tf
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Cloudflare DNS configuration
|
||||||
|
provider "cloudflare" {
|
||||||
|
api_token = var.cloudflare_api_token
|
||||||
|
}
|
||||||
|
|
||||||
|
# Data source to find the zone
|
||||||
|
data "cloudflare_zone" "main" {
|
||||||
|
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
|
||||||
|
name = var.domain_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create CNAME record for API subdomain
|
||||||
|
resource "cloudflare_record" "api" {
|
||||||
|
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
|
||||||
|
zone_id = data.cloudflare_zone.main[0].id
|
||||||
|
name = "api"
|
||||||
|
content = replace(replace(aws_api_gateway_stage.api.invoke_url, "https://", ""), "/production", "")
|
||||||
|
type = "CNAME"
|
||||||
|
ttl = 120
|
||||||
|
proxied = false
|
||||||
|
comment = "API Gateway endpoint"
|
||||||
|
}
|
||||||
52
terraform/dynamodb.tf
Normal file
52
terraform/dynamodb.tf
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# DynamoDB table for storing filament data
|
||||||
|
resource "aws_dynamodb_table" "filaments" {
|
||||||
|
name = "${var.app_name}-filaments"
|
||||||
|
billing_mode = "PAY_PER_REQUEST"
|
||||||
|
hash_key = "id"
|
||||||
|
|
||||||
|
attribute {
|
||||||
|
name = "id"
|
||||||
|
type = "S"
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute {
|
||||||
|
name = "brand"
|
||||||
|
type = "S"
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute {
|
||||||
|
name = "tip"
|
||||||
|
type = "S"
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute {
|
||||||
|
name = "status"
|
||||||
|
type = "S"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global secondary index for querying by brand
|
||||||
|
global_secondary_index {
|
||||||
|
name = "brand-index"
|
||||||
|
hash_key = "brand"
|
||||||
|
projection_type = "ALL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global secondary index for querying by type
|
||||||
|
global_secondary_index {
|
||||||
|
name = "tip-index"
|
||||||
|
hash_key = "tip"
|
||||||
|
projection_type = "ALL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global secondary index for querying by status
|
||||||
|
global_secondary_index {
|
||||||
|
name = "status-index"
|
||||||
|
hash_key = "status"
|
||||||
|
projection_type = "ALL"
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.app_name}-filaments"
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
110
terraform/lambda.tf
Normal file
110
terraform/lambda.tf
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# IAM role for Lambda functions
|
||||||
|
resource "aws_iam_role" "lambda_role" {
|
||||||
|
name = "${var.app_name}-lambda-role"
|
||||||
|
|
||||||
|
assume_role_policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Action = "sts:AssumeRole"
|
||||||
|
Effect = "Allow"
|
||||||
|
Principal = {
|
||||||
|
Service = "lambda.amazonaws.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# IAM policy for Lambda to access DynamoDB
|
||||||
|
resource "aws_iam_role_policy" "lambda_dynamodb_policy" {
|
||||||
|
name = "${var.app_name}-lambda-dynamodb-policy"
|
||||||
|
role = aws_iam_role.lambda_role.id
|
||||||
|
|
||||||
|
policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"dynamodb:GetItem",
|
||||||
|
"dynamodb:PutItem",
|
||||||
|
"dynamodb:UpdateItem",
|
||||||
|
"dynamodb:DeleteItem",
|
||||||
|
"dynamodb:Scan",
|
||||||
|
"dynamodb:Query"
|
||||||
|
]
|
||||||
|
Resource = [
|
||||||
|
aws_dynamodb_table.filaments.arn,
|
||||||
|
"${aws_dynamodb_table.filaments.arn}/index/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"logs:CreateLogGroup",
|
||||||
|
"logs:CreateLogStream",
|
||||||
|
"logs:PutLogEvents"
|
||||||
|
]
|
||||||
|
Resource = "arn:aws:logs:*:*:*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lambda function for filaments CRUD
|
||||||
|
resource "aws_lambda_function" "filaments_api" {
|
||||||
|
filename = data.archive_file.filaments_lambda_zip.output_path
|
||||||
|
function_name = "${var.app_name}-filaments-api"
|
||||||
|
role = aws_iam_role.lambda_role.arn
|
||||||
|
handler = "index.handler"
|
||||||
|
runtime = "nodejs18.x"
|
||||||
|
timeout = 30
|
||||||
|
memory_size = 256
|
||||||
|
source_code_hash = data.archive_file.filaments_lambda_zip.output_base64sha256
|
||||||
|
|
||||||
|
environment {
|
||||||
|
variables = {
|
||||||
|
TABLE_NAME = aws_dynamodb_table.filaments.name
|
||||||
|
CORS_ORIGIN = var.domain_name != "" ? "https://${var.domain_name}" : "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [aws_iam_role_policy.lambda_dynamodb_policy]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lambda function for authentication
|
||||||
|
resource "aws_lambda_function" "auth_api" {
|
||||||
|
filename = data.archive_file.auth_lambda_zip.output_path
|
||||||
|
function_name = "${var.app_name}-auth-api"
|
||||||
|
role = aws_iam_role.lambda_role.arn
|
||||||
|
handler = "index.handler"
|
||||||
|
runtime = "nodejs18.x"
|
||||||
|
timeout = 10
|
||||||
|
memory_size = 128
|
||||||
|
source_code_hash = data.archive_file.auth_lambda_zip.output_base64sha256
|
||||||
|
|
||||||
|
environment {
|
||||||
|
variables = {
|
||||||
|
JWT_SECRET = var.jwt_secret
|
||||||
|
ADMIN_USERNAME = var.admin_username
|
||||||
|
ADMIN_PASSWORD_HASH = var.admin_password_hash
|
||||||
|
CORS_ORIGIN = var.domain_name != "" ? "https://${var.domain_name}" : "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [aws_iam_role_policy.lambda_dynamodb_policy]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Archive files for Lambda deployment
|
||||||
|
data "archive_file" "filaments_lambda_zip" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/../lambda/filaments"
|
||||||
|
output_path = "${path.module}/../lambda/filaments.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
data "archive_file" "auth_lambda_zip" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/../lambda/auth"
|
||||||
|
output_path = "${path.module}/../lambda/auth.zip"
|
||||||
|
}
|
||||||
@@ -4,6 +4,10 @@ terraform {
|
|||||||
source = "hashicorp/aws"
|
source = "hashicorp/aws"
|
||||||
version = "~> 5.0"
|
version = "~> 5.0"
|
||||||
}
|
}
|
||||||
|
cloudflare = {
|
||||||
|
source = "cloudflare/cloudflare"
|
||||||
|
version = "~> 4.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
required_version = ">= 1.0"
|
required_version = ">= 1.0"
|
||||||
}
|
}
|
||||||
@@ -17,8 +21,8 @@ resource "aws_amplify_app" "filamenteka" {
|
|||||||
repository = var.github_repository
|
repository = var.github_repository
|
||||||
platform = "WEB"
|
platform = "WEB"
|
||||||
|
|
||||||
# GitHub access token for private repos
|
# GitHub access token for private repos (optional for public repos)
|
||||||
access_token = var.github_token
|
# access_token = var.github_token
|
||||||
|
|
||||||
# Build settings for Next.js
|
# Build settings for Next.js
|
||||||
build_spec = <<-EOT
|
build_spec = <<-EOT
|
||||||
@@ -48,6 +52,7 @@ resource "aws_amplify_app" "filamenteka" {
|
|||||||
CONFLUENCE_API_URL = var.confluence_api_url
|
CONFLUENCE_API_URL = var.confluence_api_url
|
||||||
CONFLUENCE_TOKEN = var.confluence_token
|
CONFLUENCE_TOKEN = var.confluence_token
|
||||||
CONFLUENCE_PAGE_ID = var.confluence_page_id
|
CONFLUENCE_PAGE_ID = var.confluence_page_id
|
||||||
|
NEXT_PUBLIC_API_URL = aws_api_gateway_stage.api.invoke_url
|
||||||
}
|
}
|
||||||
|
|
||||||
# Custom rules for single-page app
|
# Custom rules for single-page app
|
||||||
|
|||||||
@@ -18,3 +18,17 @@ output "custom_domain_url" {
|
|||||||
value = var.domain_name != "" ? "https://${var.domain_name}" : "Not configured"
|
value = var.domain_name != "" ? "https://${var.domain_name}" : "Not configured"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "api_url" {
|
||||||
|
description = "API Gateway URL"
|
||||||
|
value = aws_api_gateway_stage.api.invoke_url
|
||||||
|
}
|
||||||
|
|
||||||
|
output "dynamodb_table_name" {
|
||||||
|
description = "DynamoDB table name"
|
||||||
|
value = aws_dynamodb_table.filaments.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "api_custom_url" {
|
||||||
|
description = "Custom API URL via Cloudflare"
|
||||||
|
value = var.domain_name != "" && var.cloudflare_api_token != "" ? "https://api.${var.domain_name}" : "Use api_url output instead"
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,5 +7,10 @@ confluence_api_url = "https://your-domain.atlassian.net"
|
|||||||
confluence_token = "your_confluence_api_token"
|
confluence_token = "your_confluence_api_token"
|
||||||
confluence_page_id = "your_confluence_page_id"
|
confluence_page_id = "your_confluence_page_id"
|
||||||
|
|
||||||
|
# Admin Authentication
|
||||||
|
jwt_secret = "your-secret-key-at-least-32-characters-long"
|
||||||
|
admin_username = "admin"
|
||||||
|
admin_password_hash = "bcrypt-hash-generated-by-generate-password-hash.js"
|
||||||
|
|
||||||
# Optional: Custom domain
|
# Optional: Custom domain
|
||||||
# domain_name = "filamenteka.yourdomain.com"
|
# domain_name = "filamenteka.yourdomain.com"
|
||||||
@@ -35,4 +35,35 @@ variable "environment" {
|
|||||||
description = "Environment name"
|
description = "Environment name"
|
||||||
type = string
|
type = string
|
||||||
default = "production"
|
default = "production"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_name" {
|
||||||
|
description = "Application name"
|
||||||
|
type = string
|
||||||
|
default = "filamenteka"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "jwt_secret" {
|
||||||
|
description = "JWT secret for authentication"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "admin_username" {
|
||||||
|
description = "Admin username"
|
||||||
|
type = string
|
||||||
|
default = "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "admin_password_hash" {
|
||||||
|
description = "BCrypt hash of admin password"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cloudflare_api_token" {
|
||||||
|
description = "Cloudflare API token for DNS management"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
sensitive = true
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user