diff --git a/.env.production b/.env.production index b5bd761..d95aac8 100644 --- a/.env.production +++ b/.env.production @@ -1,5 +1,3 @@ # This file is for Amplify to know which env vars to expose to Next.js # The actual values come from Amplify Environment Variables -CONFLUENCE_API_URL=${CONFLUENCE_API_URL} -CONFLUENCE_TOKEN=${CONFLUENCE_TOKEN} -CONFLUENCE_PAGE_ID=${CONFLUENCE_PAGE_ID} \ No newline at end of file +NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} \ No newline at end of file diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx new file mode 100644 index 0000000..8f9e6f9 --- /dev/null +++ b/app/admin/dashboard/page.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [editingFilament, setEditingFilament] = useState(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) => { + 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 ( +
+
+
+ ); + } + + return ( +
+
+
+
+

+ Admin Dashboard +

+
+ + +
+
+
+
+ +
+ {error && ( +
+ {error} +
+ )} + + {/* Add/Edit Form */} + {(showAddForm || editingFilament) && ( + { + setEditingFilament(null); + setShowAddForm(false); + }} + /> + )} + + {/* Filaments Table */} +
+ + + + + + + + + + + + + + + + + {filaments.map((filament) => ( + + + + + + + + + + + + + ))} + +
BrandTipFinishBojaRefillVakumOtvorenoKoličinaCenaAkcije
{filament.brand}{filament.tip}{filament.finish}{filament.boja}{filament.refill}{filament.vakum}{filament.otvoreno}{filament.kolicina}{filament.cena} + + +
+
+
+
+ ); +} + +// Filament Form Component +function FilamentForm({ + filament, + onSave, + onCancel +}: { + filament: Partial, + onSave: (filament: Partial) => 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) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave({ + ...filament, + ...formData + }); + }; + + return ( +
+

+ {filament.id ? 'Izmeni filament' : 'Dodaj novi filament'} +

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..80978bd --- /dev/null +++ b/app/admin/page.tsx @@ -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 ( +
+
+
+

+ Admin Prijava +

+

+ Prijavite se za upravljanje filamentima +

+
+
+ {error && ( +
+

{error}

+
+ )} +
+
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index e175831..94cb1ae 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,31 +10,42 @@ export default function Home() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdate, setLastUpdate] = useState(null); - const [darkMode, setDarkMode] = useState(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('darkMode'); - return saved ? JSON.parse(saved) : false; - } - return false; - }); + const [darkMode, setDarkMode] = useState(false); + const [mounted, setMounted] = useState(false); + // Initialize dark mode from localStorage after mounting useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('darkMode', JSON.stringify(darkMode)); - if (darkMode) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); - } + setMounted(true); + const saved = localStorage.getItem('darkMode'); + if (saved) { + setDarkMode(JSON.parse(saved)); } - }, [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 () => { try { setLoading(true); 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); setLastUpdate(new Date()); } catch (err) { @@ -74,13 +85,15 @@ export default function Home() { > {loading ? 'Ažuriranje...' : 'Osveži'} - + {mounted && ( + + )} diff --git a/lambda/auth.zip b/lambda/auth.zip new file mode 100644 index 0000000..5f033bc Binary files /dev/null and b/lambda/auth.zip differ diff --git a/lambda/auth/index.js b/lambda/auth/index.js new file mode 100644 index 0000000..9e957ea --- /dev/null +++ b/lambda/auth/index.js @@ -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' }); +}; \ No newline at end of file diff --git a/lambda/auth/package-lock.json b/lambda/auth/package-lock.json new file mode 100644 index 0000000..8057b7c --- /dev/null +++ b/lambda/auth/package-lock.json @@ -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" + } + } + } +} diff --git a/lambda/auth/package.json b/lambda/auth/package.json new file mode 100644 index 0000000..ba55de3 --- /dev/null +++ b/lambda/auth/package.json @@ -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" +} diff --git a/lambda/filaments.zip b/lambda/filaments.zip new file mode 100644 index 0000000..5a99612 Binary files /dev/null and b/lambda/filaments.zip differ diff --git a/lambda/filaments/index.js b/lambda/filaments/index.js new file mode 100644 index 0000000..f1f8bca --- /dev/null +++ b/lambda/filaments/index.js @@ -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' }); +}; \ No newline at end of file diff --git a/lambda/filaments/package-lock.json b/lambda/filaments/package-lock.json new file mode 100644 index 0000000..a440bb8 --- /dev/null +++ b/lambda/filaments/package-lock.json @@ -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" + } + } + } +} diff --git a/lambda/filaments/package.json b/lambda/filaments/package.json new file mode 100644 index 0000000..2071706 --- /dev/null +++ b/lambda/filaments/package.json @@ -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" +} diff --git a/package-lock.json b/package-lock.json index 55d3165..18c5faf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/cheerio": "^0.22.35", "axios": "^1.6.2", + "bcryptjs": "^3.0.2", "cheerio": "^1.1.0", "next": "^15.3.4", "react": "^19.1.0", @@ -4214,6 +4215,15 @@ "dev": true, "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", diff --git a/package.json b/package.json index 05fecdd..7b03987 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,14 @@ "test:watch": "jest --watch", "security:check": "node scripts/security-check.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": { "@types/cheerio": "^0.22.35", "axios": "^1.6.2", + "bcryptjs": "^3.0.2", "cheerio": "^1.1.0", "next": "^15.3.4", "react": "^19.1.0", diff --git a/public/data.json b/public/data.json new file mode 100644 index 0000000..63fe1be --- /dev/null +++ b/public/data.json @@ -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" + } +] \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..a6b0681 --- /dev/null +++ b/scripts/README.md @@ -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 \ No newline at end of file diff --git a/scripts/generate-password-hash.js b/scripts/generate-password-hash.js new file mode 100644 index 0000000..bd6ea83 --- /dev/null +++ b/scripts/generate-password-hash.js @@ -0,0 +1,13 @@ +const bcrypt = require('bcryptjs'); + +const password = process.argv[2]; + +if (!password) { + console.error('Usage: node generate-password-hash.js '); + 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}"`); \ No newline at end of file diff --git a/scripts/migrate-confluence-to-dynamo.js b/scripts/migrate-confluence-to-dynamo.js new file mode 100644 index 0000000..3b9d2e5 --- /dev/null +++ b/scripts/migrate-confluence-to-dynamo.js @@ -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>/gs; + const cellRegex = /]*>(.*?)<\/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(); +} \ No newline at end of file diff --git a/scripts/migrate-with-parser.js b/scripts/migrate-with-parser.js new file mode 100644 index 0000000..77281b1 --- /dev/null +++ b/scripts/migrate-with-parser.js @@ -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(); +} \ No newline at end of file diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 0000000..93b16b5 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,1019 @@ +{ + "name": "filamenteka-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "filamenteka-scripts", + "version": "1.0.0", + "dependencies": { + "aws-sdk": "^2.1472.0", + "axios": "^1.6.2", + "cheerio": "^1.0.0-rc.12", + "dotenv": "^16.3.1", + "uuid": "^9.0.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "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/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "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/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "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/cheerio": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz", + "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.10.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "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/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "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/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "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/undici": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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" + } + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..5802c89 --- /dev/null +++ b/scripts/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/components/FilamentTable.tsx b/src/components/FilamentTable.tsx index c35b88b..76ca44d 100644 --- a/src/components/FilamentTable.tsx +++ b/src/components/FilamentTable.tsx @@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react'; import { Filament } from '../types/filament'; import { ColorCell } from './ColorCell'; import { getFilamentColor, getColorStyle, getContrastColor } from '../data/bambuLabColors'; +import '../styles/select.css'; interface FilamentTableProps { filaments: Filament[]; @@ -13,13 +14,48 @@ export const FilamentTable: React.FC = ({ filaments, loading const [searchTerm, setSearchTerm] = useState(''); const [sortField, setSortField] = useState('boja'); 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(() => { - let filtered = filaments.filter(filament => - Object.values(filament).some(value => + let filtered = filaments.filter(filament => { + // Search filter + const matchesSearch = Object.values(filament).some(value => 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) => { const aValue = a[sortField]; @@ -33,7 +69,7 @@ export const FilamentTable: React.FC = ({ filaments, loading }); return filtered; - }, [filaments, searchTerm, sortField, sortOrder]); + }, [filaments, searchTerm, sortField, sortOrder, filterBrand, filterTip, filterFinish, filterStatus]); const handleSort = (field: keyof Filament) => { if (sortField === field) { @@ -62,14 +98,103 @@ export const FilamentTable: React.FC = ({ filaments, loading return (
-
- 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" - /> +
+ {/* Search Bar */} +
+ 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" + /> +
+ + {/* Filter Row */} +
+ {/* Brand Filter */} +
+ + +
+ + {/* Type Filter */} +
+ + +
+ + {/* Finish Filter */} +
+ + +
+ + {/* Status Filter */} +
+ + +
+
+ + {/* Clear Filters Button */} + {(filterBrand || filterTip || filterFinish || filterStatus) && ( + + )}
diff --git a/src/styles/select.css b/src/styles/select.css new file mode 100644 index 0000000..4131ff1 --- /dev/null +++ b/src/styles/select.css @@ -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; + } + } +} \ No newline at end of file diff --git a/terraform/api_gateway.tf b/terraform/api_gateway.tf new file mode 100644 index 0000000..998713f --- /dev/null +++ b/terraform/api_gateway.tf @@ -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 +} \ No newline at end of file diff --git a/terraform/cloudflare-dns.tf b/terraform/cloudflare-dns.tf new file mode 100644 index 0000000..00ab07e --- /dev/null +++ b/terraform/cloudflare-dns.tf @@ -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" +} \ No newline at end of file diff --git a/terraform/dynamodb.tf b/terraform/dynamodb.tf new file mode 100644 index 0000000..e4b9219 --- /dev/null +++ b/terraform/dynamodb.tf @@ -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 + } +} \ No newline at end of file diff --git a/terraform/lambda.tf b/terraform/lambda.tf new file mode 100644 index 0000000..665db64 --- /dev/null +++ b/terraform/lambda.tf @@ -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" +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf index 9a07461..e1af232 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -4,6 +4,10 @@ terraform { source = "hashicorp/aws" version = "~> 5.0" } + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 4.0" + } } required_version = ">= 1.0" } @@ -17,8 +21,8 @@ resource "aws_amplify_app" "filamenteka" { repository = var.github_repository platform = "WEB" - # GitHub access token for private repos - access_token = var.github_token + # GitHub access token for private repos (optional for public repos) + # access_token = var.github_token # Build settings for Next.js build_spec = <<-EOT @@ -48,6 +52,7 @@ resource "aws_amplify_app" "filamenteka" { CONFLUENCE_API_URL = var.confluence_api_url CONFLUENCE_TOKEN = var.confluence_token CONFLUENCE_PAGE_ID = var.confluence_page_id + NEXT_PUBLIC_API_URL = aws_api_gateway_stage.api.invoke_url } # Custom rules for single-page app diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 6dec7e3..d4be428 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -18,3 +18,17 @@ output "custom_domain_url" { 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" +} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example index b19fecc..4801e1c 100644 --- a/terraform/terraform.tfvars.example +++ b/terraform/terraform.tfvars.example @@ -7,5 +7,10 @@ confluence_api_url = "https://your-domain.atlassian.net" confluence_token = "your_confluence_api_token" 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 # domain_name = "filamenteka.yourdomain.com" \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf index 2bb2600..fa71af6 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -35,4 +35,35 @@ variable "environment" { description = "Environment name" type = string 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 } \ No newline at end of file