diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..91b9609 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,118 @@ +name: Build and Publish Docker Image + +on: + push: + branches: [ "main", "develop" ] + paths: [ "backend/**" ] + pull_request: + branches: [ "main" ] + paths: [ "backend/**" ] + release: + types: [ published ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/fizika-admin + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix={{branch}}- + # set latest tag for default branch + type=raw,value=latest,enable={{is_default_branch}} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + # Security scanning + sbom: true + provenance: true + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v3.1.1 + with: + cosign-release: 'v2.1.1' + + - name: Sign the published Docker image + if: github.event_name != 'pull_request' + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} + + security-scan: + runs-on: ubuntu-latest + needs: build-and-push + if: github.event_name != 'pull_request' + permissions: + security-events: write + + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file diff --git a/README.md b/README.md index dff22d4..afd3a86 100644 --- a/README.md +++ b/README.md @@ -1 +1,205 @@ -# Fizika weboldal +# Fizika - Physics Quiz Application + +A comprehensive physics quiz application for Hungarian students preparing for their physics exams (érettségi). The application consists of a frontend quiz interface and an admin backend for content management. + +## 🚀 Features + +### Student Interface (Frontend) +- Interactive physics quiz questions +- Multiple choice questions with immediate feedback +- Category-based filtering (dynamics, mechanics, fluids, etc.) +- Search functionality by year, month, and question number +- Responsive design for desktop and mobile +- Progress tracking and scoring +- Timer functionality + +### Admin Interface (Backend) +- 📝 Full CRUD operations for quiz questions +- 🖼️ Image management (upload, view, delete) +- 📊 RESTful API for frontend integration +- 🛡️ Basic security features (input validation) +- 🐳 Docker containerization ready + +## 📁 Project Structure + +``` +fizika/ +├── index.html # Main quiz interface +├── fizika.json # Quiz questions database +├── pics/ # Question images +├── js/ # Frontend JavaScript +│ ├── fizika.js # Main quiz logic +│ └── load.js # Data loading (updated for API) +├── css/ # Stylesheets +├── backend/ # Admin backend service +│ ├── server.js # Express server +│ ├── public/admin.html # Admin interface +│ ├── package.json # Dependencies +│ ├── Dockerfile # Container configuration +│ └── README.md # Backend documentation +├── docker-compose.yml # Multi-service setup +└── .github/workflows/ # CI/CD pipelines +``` + +## 🛠️ Quick Start + +### Option 1: Docker (Recommended) + +1. **Clone the repository:** + ```bash + git clone + cd fizika + ``` + +2. **Start the services:** + ```bash + docker-compose up -d + ``` + +3. **Access the applications:** + - **Student Quiz**: http://localhost (or your domain) + - **Admin Panel**: http://localhost:3001/admin.html + +### Option 2: Local Development + +1. **Frontend** (Static files): + - Serve the root directory with any web server + - Update `API_BASE` in `js/load.js` to point to your backend + +2. **Backend** (Admin API): + ```bash + cd backend + npm install + cp .env.example .env # Configure your environment + npm run dev + ``` + +## 🔧 Configuration + +### Backend Environment Variables + +Create `backend/.env` from the example: + +```bash +# Server settings +PORT=3001 +NODE_ENV=production + +# Frontend integration +FRONTEND_URL=* +``` + +## 🔌 API Integration + +The frontend automatically detects the backend: +- **Local Development**: `http://localhost:3001` +- **Production**: Configure `API_BASE` in `js/load.js` + +If the backend is unavailable, the frontend falls back to loading `fizika.json` directly. + +## 📊 API Endpoints + +### Public (Used by Frontend) +- `GET /api/fizika` - Get all questions +- `GET /api/images` - List images +- `GET /api/pics/:filename` - Serve images + +### Admin (Open Access) +- `GET|POST|PUT|DELETE /api/admin/questions` - Question management +- `POST|DELETE /api/admin/images` - Image management + +## 🐳 Docker Deployment + +### Production with Docker Compose + +```bash +# Deploy +docker-compose up -d + +# With nginx reverse proxy +docker-compose --profile nginx up -d +``` + +### Manual Docker Build + +```bash +docker build -t fizika-admin ./backend +docker run -d -p 3001:3001 \ + -v $(pwd)/fizika.json:/usr/src/app/fizika.json:ro \ + -v $(pwd)/pics:/usr/src/app/pics \ + fizika-admin +``` + +## 🔄 CI/CD Pipeline + +GitHub Actions automatically: +- Builds Docker images on pushes to main +- Publishes to GitHub Container Registry +- Signs images with Cosign for security +- Performs vulnerability scanning +- Supports multi-architecture builds (AMD64, ARM64) + +## 🧪 Data Structure + +Questions in `fizika.json` follow this format: + +```json +{ + "id": 1, + "source": "2016/m1/1", + "description": "Question text...", + "a": "Option A", + "b": "Option B", + "c": "Option C", + "d": "Option D", + "correct": 2, + "type": "md", + "image": "optional-image.jpg" +} +``` + +### Question Types +- `md` - Dinamika (Dynamics) +- `me` - Mechanika (Mechanics) +- `mf` - Folyadékok (Fluids) + +## 🛡️ Security Features + +- Input validation and sanitization +- CORS protection with configurable origins +- Secure file upload with type/size restrictions +- Security headers via Helmet.js +- Container security with non-root user + +## 🎯 Usage Scenarios + +1. **Student Practice**: Access main site, select categories, take quizzes +2. **Content Management**: Login to admin panel, add/edit questions and images +3. **Deployment**: Use Docker Compose for easy production deployment +4. **Development**: Use dev profile for hot-reload development + +## 📚 Documentation + +- **Backend API**: See `backend/README.md` for detailed API documentation +- **Frontend**: Static HTML/JS application with jQuery +- **Docker**: Multi-stage builds with security best practices +- **CI/CD**: Automated builds and deployments via GitHub Actions + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## 📄 License + +Educational use for Hungarian physics students. + +--- + +**Open Admin Access** +- No authentication required +- Direct access to admin panel +- ⚠️ **Secure admin access with reverse proxy in production!** diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..942162e --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +node_modules +Dockerfile +.dockerignore \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..64159d5 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,6 @@ +# Server Configuration +PORT=3001 +NODE_ENV=development + +# CORS Configuration +FRONTEND_URL=* \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6c697b9 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,61 @@ +# Multi-stage Dockerfile for Fizika Admin Backend +FROM node:18-alpine AS base + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Create app directory +WORKDIR /usr/src/app + +# Create non-root user +RUN addgroup -g 1001 -S nodejs +RUN adduser -S fizika -u 1001 + +# Copy package files +COPY package*.json ./ + +# Development stage +FROM base AS dependencies + +# Install dependencies +RUN npm ci --only=production && npm cache clean --force + +# Development dependencies for building +FROM base AS dev-dependencies +RUN npm ci + +# Production stage +FROM base AS production + +# Copy production dependencies +COPY --from=dependencies /usr/src/app/node_modules ./node_modules + +# Copy application code +COPY --chown=fizika:nodejs . . + +# Create necessary directories and set permissions +RUN mkdir -p /usr/src/app/data /usr/src/app/pics && \ + chown -R fizika:nodejs /usr/src/app/data /usr/src/app/pics + +# Security: Remove package files and any other sensitive data +RUN rm -f package*.json + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3001 + +# Expose port +EXPOSE 3001 + +# Switch to non-root user +USER fizika + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3001/api/fizika', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))" + +# Use dumb-init for proper signal handling +ENTRYPOINT ["dumb-init", "--"] + +# Start the application +CMD ["node", "server.js"] \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..6d67a14 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,239 @@ +# Fizika Admin Backend + +A secure Node.js/Express backend for managing physics quiz questions and images for the Fizika website. + +## Features + +- 🔒 **JWT-based Authentication** - Secure admin access with token-based auth +- 📝 **Question Management** - Full CRUD operations for quiz questions +- 🖼️ **Image Management** - Upload, view, and delete image files +- 🛡️ **Security First** - Rate limiting, input validation, CORS protection +- 📊 **Public API** - Serves data to the frontend application +- 🐳 **Docker Ready** - Containerized with multi-stage build +- 🚀 **Production Ready** - Health checks, proper error handling, logging + +## API Endpoints + +### Public Endpoints +- `GET /api/fizika` - Get all questions (for frontend) +- `GET /api/images` - List all available images +- `GET /api/pics/:filename` - Serve image files + +### Authentication +- `POST /api/auth/login` - Admin login (returns JWT token) + +### Admin Endpoints (Require Authentication) +- `GET /api/admin/questions` - Get all questions +- `POST /api/admin/questions` - Create new question +- `PUT /api/admin/questions/:id` - Update question +- `DELETE /api/admin/questions/:id` - Delete question +- `POST /api/admin/images/upload` - Upload image +- `DELETE /api/admin/images/:filename` - Delete image + +## Quick Start + +### Using Docker (Recommended) + +1. **Clone and navigate to the project:** + ```bash + git clone + cd fizika + ``` + +2. **Set up environment variables:** + ```bash + cp backend/.env.example backend/.env + # Edit backend/.env with your configuration + ``` + +3. **Run with Docker Compose:** + ```bash + # Production mode + docker-compose up -d + + # Development mode with hot reload + docker-compose --profile dev up fizika-admin-dev + ``` + +4. **Access the admin interface:** + - Open http://localhost:3001/admin.html + - Default password: `admin123` (change this!) + +### Local Development + +1. **Install dependencies:** + ```bash + cd backend + npm install + ``` + +2. **Set up environment:** + ```bash + cp .env.example .env + # Edit .env file with your settings + ``` + +3. **Run the server:** + ```bash + npm run dev # Development with nodemon + npm start # Production mode + ``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `PORT` | Server port | `3001` | +| `NODE_ENV` | Environment | `development` | +| `JWT_SECRET` | JWT signing secret | *Required in production* | +| `ADMIN_PASSWORD_HASH` | Bcrypt hash of admin password | `admin123` | +| `FRONTEND_URL` | CORS origin for frontend | `http://localhost:8080` | + +### Generating Password Hash + +```bash +node -e "console.log(require('bcrypt').hashSync('your-password', 10))" +``` + +## Security Features + +- **Rate Limiting**: 100 requests per 15 minutes, 5 auth attempts per 15 minutes +- **Input Validation**: All inputs validated and sanitized +- **File Upload Security**: Image-only uploads with size limits (5MB) +- **JWT Authentication**: Secure token-based admin authentication +- **CORS Protection**: Configurable cross-origin request handling +- **Helmet.js**: Security headers and protection middleware + +## Data Structure + +### Question Object +```json +{ + "id": 1, + "source": "2016/m1/1", + "description": "Question text...", + "a": "Option A", + "b": "Option B", + "c": "Option C", + "d": "Option D", + "correct": 2, + "type": "md", + "image": "image.jpg" +} +``` + +### Question Types +- `md` - Dinamika (Dynamics) +- `me` - Mechanika (Mechanics) +- `mf` - Folyadékok (Fluids) + +## Docker Deployment + +### Building the Image +```bash +docker build -t fizika-admin ./backend +``` + +### Running with Docker +```bash +docker run -d \ + -p 3001:3001 \ + -e JWT_SECRET=your-secret-key \ + -e ADMIN_PASSWORD_HASH='$2b$10$...' \ + -v $(pwd)/fizika.json:/usr/src/app/fizika.json:ro \ + -v $(pwd)/pics:/usr/src/app/pics \ + fizika-admin +``` + +### Docker Compose Production +```bash +# Set environment variables +export JWT_SECRET="your-super-secret-jwt-key" +export ADMIN_PASSWORD_HASH="$2b$10$your-bcrypt-hash" +export FRONTEND_URL="https://your-domain.com" + +# Run +docker-compose up -d +``` + +## GitHub Actions + +The project includes a GitHub Actions workflow that: +- Builds multi-architecture Docker images (AMD64, ARM64) +- Pushes to GitHub Container Registry +- Signs images with Cosign +- Performs security scanning with Trivy +- Runs on pushes to main branch and releases + +## Admin Interface + +Access the admin interface at `/admin.html`: +- **Questions Tab**: Add, edit, delete quiz questions +- **Images Tab**: Upload and manage image files +- **Responsive Design**: Works on desktop and mobile +- **Real-time Updates**: Changes reflect immediately + +## API Client Example + +```javascript +// Login +const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password: 'your-password' }) +}); +const { token } = await response.json(); + +// Create question +await fetch('/api/admin/questions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + source: '2024/test/1', + description: 'What is physics?', + a: 'Science', + b: 'Art', + c: 'Math', + d: 'Biology', + correct: 1, + type: 'md' + }) +}); +``` + +## Troubleshooting + +### Common Issues + +1. **CORS Errors**: Check `FRONTEND_URL` environment variable +2. **Authentication Fails**: Verify `JWT_SECRET` and `ADMIN_PASSWORD_HASH` +3. **File Upload Errors**: Check write permissions on pics directory +4. **Health Check Fails**: Ensure fizika.json exists and is readable + +### Logs +```bash +# Docker logs +docker logs fizika-admin + +# Docker Compose logs +docker-compose logs fizika-admin +``` + +## License + +This project is part of the Fizika educational platform. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +For issues or questions, please open a GitHub issue. \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..25feaa4 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,66 @@ +version: '3.8' + +services: + fizika-admin: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "3001:3001" + environment: + - NODE_ENV=production + - PORT=3001 + - FRONTEND_URL=${FRONTEND_URL:-*} + volumes: + # Mount data files + - ./fizika.json:/usr/src/app/fizika.json:ro + - ./pics:/usr/src/app/pics + # Optional: mount for development + # - ./backend:/usr/src/app + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/api/fizika', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Optional: Development service with hot reload + fizika-admin-dev: + build: + context: ./backend + dockerfile: Dockerfile + target: dev-dependencies + ports: + - "3001:3001" + environment: + - NODE_ENV=development + - PORT=3001 + - FRONTEND_URL=* + volumes: + - ./backend:/usr/src/app + - ./fizika.json:/usr/src/app/fizika.json + - ./pics:/usr/src/app/pics + - /usr/src/app/node_modules + command: npm run dev + profiles: ["dev"] + + # Optional: Nginx reverse proxy for production + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - .:/usr/share/nginx/html:ro + # SSL certificates (if using HTTPS) + # - ./ssl:/etc/ssl/certs + depends_on: + - fizika-admin + restart: unless-stopped + profiles: ["nginx"] + +networks: + default: + name: fizika-network \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..d0ede03 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1406 @@ +{ + "name": "fizika-admin-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fizika-admin-backend", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "helmet": "^7.1.0", + "multer": "^1.4.5-lts.1" + }, + "devDependencies": { + "nodemon": "^3.0.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/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/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "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/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/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..bb2175e --- /dev/null +++ b/backend/package.json @@ -0,0 +1,22 @@ +{ + "name": "fizika-admin-backend", + "version": "1.0.0", + "description": "Backend for editing Fizika quiz data", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.2", + "multer": "^1.4.5-lts.1", + "cors": "^2.8.5", + "helmet": "^7.1.0" + }, + "devDependencies": { + "nodemon": "^3.0.2" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/backend/public/admin.html b/backend/public/admin.html new file mode 100644 index 0000000..ce48d3b --- /dev/null +++ b/backend/public/admin.html @@ -0,0 +1,351 @@ + + + + + + Fizika Admin - Kérdések és képek kezelése + + + +
+
+

Fizika Admin Panel

+

Kérdések és képek kezelése

+
+ + +
+ + +
+ + +
+

Kérdések kezelése

+
+ + + +
+
Kérdések betöltése...
+
+
+ + +
+

Képek kezelése

+
+ +
+ + +
+ +
+
Képek betöltése...
+
+
+
+ + + + + + + diff --git a/backend/public/admin.js b/backend/public/admin.js new file mode 100644 index 0000000..4bc5958 --- /dev/null +++ b/backend/public/admin.js @@ -0,0 +1,324 @@ +const API_BASE = window.location.origin; + +// Initialize +document.addEventListener("DOMContentLoaded", function () { + loadQuestions(); + loadImages(); + + // Set up event listeners + setupEventListeners(); +}); + +function setupEventListeners() { + // Tab switching + document.querySelectorAll('.tab[data-tab]').forEach(tab => { + tab.addEventListener('click', (e) => { + switchTab(e.target.dataset.tab); + }); + }); + + // Add question button + document.getElementById('addQuestionBtn').addEventListener('click', showAddQuestionModal); + + // Image upload + document.getElementById('imageUpload').addEventListener('change', uploadImage); + + // Modal close buttons + document.getElementById('closeModalBtn').addEventListener('click', closeModal); + document.getElementById('cancelBtn').addEventListener('click', closeModal); + + // Question form submit + document.getElementById('questionForm').addEventListener('submit', saveQuestion); + + // Close modal when clicking outside + document.getElementById('questionModal').addEventListener('click', (e) => { + if (e.target.id === 'questionModal') { + closeModal(); + } + }); +} + +// Tab switching +function switchTab(tabName) { + document + .querySelectorAll(".tab-content") + .forEach((tab) => tab.classList.remove("active")); + document + .querySelectorAll(".tab") + .forEach((tab) => tab.classList.remove("active")); + + document.getElementById(tabName + "Tab").classList.add("active"); + document.querySelector(`[data-tab="${tabName}"]`).classList.add("active"); + + if (tabName === "images") loadImages(); +} + +// Questions management +async function loadQuestions() { + try { + const response = await fetch(`${API_BASE}/api/admin/questions`); + if (response.ok) { + const questions = await response.json(); + displayQuestions(questions); + } else { + throw new Error("Failed to load"); + } + } catch (error) { + document.getElementById("questionsList").innerHTML = + '
Hiba a kérdések betöltésekor
'; + } +} + +function displayQuestions(questions) { + const container = document.getElementById("questionsList"); + container.innerHTML = questions + .map( + (q) => ` +
+

ID: ${q.id} - ${q.source}

+

Kérdés: ${q.description.substring( + 0, + 100 + )}...

+

Típus: ${ + q.type + } | Helyes válasz: ${ + ["A", "B", "C", "D"][q.correct - 1] + }

+
+ + +
+
+ ` + ) + .join(""); + + // Add event listeners for edit and delete buttons + container.querySelectorAll('[data-edit-id]').forEach(btn => { + btn.addEventListener('click', (e) => { + editQuestion(parseInt(e.target.dataset.editId)); + }); + }); + + container.querySelectorAll('[data-delete-id]').forEach(btn => { + btn.addEventListener('click', (e) => { + deleteQuestion(parseInt(e.target.dataset.deleteId)); + }); + }); +} + +function showAddQuestionModal() { + document.getElementById("modalTitle").textContent = + "Új kérdés hozzáadása"; + document.getElementById("questionForm").reset(); + document.getElementById("questionId").value = ""; + document.getElementById("questionModal").style.display = "block"; +} + +async function editQuestion(id) { + try { + const response = await fetch(`${API_BASE}/api/admin/questions`); + if (response.ok) { + const questions = await response.json(); + const question = questions.find((q) => q.id === id); + + if (question) { + document.getElementById("modalTitle").textContent = + "Kérdés szerkesztése"; + document.getElementById("questionId").value = question.id; + document.getElementById("questionSource").value = question.source; + document.getElementById("questionDescription").value = + question.description; + document.getElementById("questionA").value = question.a; + document.getElementById("questionB").value = question.b; + document.getElementById("questionC").value = question.c; + document.getElementById("questionD").value = question.d; + document.getElementById("questionCorrect").value = + question.correct; + document.getElementById("questionType").value = question.type; + document.getElementById("questionImage").value = + question.image || ""; + document.getElementById("questionModal").style.display = "block"; + } + } + } catch (error) { + showAlert("questionsAlert", "Hiba a kérdés betöltésekor", "danger"); + } +} + +async function saveQuestion(event) { + event.preventDefault(); + + const data = { + source: document.getElementById("questionSource").value, + description: document.getElementById("questionDescription").value, + a: document.getElementById("questionA").value, + b: document.getElementById("questionB").value, + c: document.getElementById("questionC").value, + d: document.getElementById("questionD").value, + correct: parseInt(document.getElementById("questionCorrect").value), + type: document.getElementById("questionType").value, + image: document.getElementById("questionImage").value || null, + }; + + const id = document.getElementById("questionId").value; + const isEdit = id !== ""; + + try { + const response = await fetch( + isEdit + ? `${API_BASE}/api/admin/questions/${id}` + : `${API_BASE}/api/admin/questions`, + { + method: isEdit ? "PUT" : "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + } + ); + + if (response.ok) { + closeModal(); + loadQuestions(); + showAlert("questionsAlert", "Kérdés sikeresen mentve!", "success"); + } else { + const error = await response.json(); + showAlert( + "questionsAlert", + error.error || "Hiba a mentés során", + "danger" + ); + } + } catch (error) { + showAlert("questionsAlert", "Kapcsolat hiba", "danger"); + } +} + +async function deleteQuestion(id) { + if (!confirm("Biztosan törölni szeretnéd ezt a kérdést?")) return; + + try { + const response = await fetch( + `${API_BASE}/api/admin/questions/${id}`, + { method: "DELETE" } + ); + if (response.ok) { + loadQuestions(); + showAlert("questionsAlert", "Kérdés törölve!", "success"); + } else { + throw new Error("Delete failed"); + } + } catch (error) { + showAlert("questionsAlert", "Hiba a törlés során", "danger"); + } +} + +// Images management +async function loadImages() { + try { + const response = await fetch(`${API_BASE}/api/images`); + if (response.ok) { + const images = await response.json(); + displayImages(images); + } else { + throw new Error("Failed to load"); + } + } catch (error) { + document.getElementById("imagesList").innerHTML = + '
Hiba a képek betöltésekor
'; + } +} + +function displayImages(images) { + const container = document.getElementById("imagesList"); + container.innerHTML = images + .map( + (image) => ` +
+ ${image} +

${image}

+ +
+ ` + ) + .join(""); + + // Add event listeners for delete buttons + container.querySelectorAll('[data-delete-image]').forEach(btn => { + btn.addEventListener('click', (e) => { + deleteImage(e.target.dataset.deleteImage); + }); + }); +} + +async function uploadImage() { + const input = document.getElementById("imageUpload"); + const file = input.files[0]; + + if (!file) return; + + const formData = new FormData(); + formData.append("image", file); + + try { + const response = await fetch(`${API_BASE}/api/admin/images/upload`, { + method: "POST", + body: formData, + }); + + if (response.ok) { + input.value = ""; + loadImages(); + showAlert("imagesAlert", "Kép sikeresen feltöltve!", "success"); + } else { + const error = await response.json(); + showAlert( + "imagesAlert", + error.error || "Hiba a feltöltés során", + "danger" + ); + } + } catch (error) { + showAlert("imagesAlert", "Kapcsolat hiba", "danger"); + } +} + +async function deleteImage(filename) { + if (!confirm(`Biztosan törölni szeretnéd a ${filename} képet?`)) return; + + try { + const response = await fetch( + `${API_BASE}/api/admin/images/${encodeURIComponent(filename)}`, + { + method: "DELETE", + } + ); + + if (response.ok) { + loadImages(); + showAlert("imagesAlert", "Kép törölve!", "success"); + } else { + throw new Error("Delete failed"); + } + } catch (error) { + showAlert("imagesAlert", "Hiba a törlés során", "danger"); + } +} + +// Utility functions +function closeModal() { + document.getElementById("questionModal").style.display = "none"; +} + +function showAlert(elementId, message, type) { + const alertDiv = document.getElementById(elementId); + alertDiv.className = `alert alert-${type}`; + alertDiv.textContent = message; + alertDiv.style.display = "block"; + + setTimeout(() => { + alertDiv.style.display = "none"; + }, 5000); +} + +// This is now handled in setupEventListeners() \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..2cef01d --- /dev/null +++ b/backend/server.js @@ -0,0 +1,183 @@ +const express = require('express'); +const cors = require('cors'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs').promises; +const helmet = require('helmet'); + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Security middleware +app.use(helmet()); +app.use(cors({ + origin: process.env.FRONTEND_URL || '*', + credentials: true +})); + +app.use(express.json({ limit: '10mb' })); +app.use(express.static('public')); + +// File paths +const DATA_PATH = path.join(__dirname, '../frontend/fizika.json'); +const PICS_PATH = path.join(__dirname, '../frontend/pics'); + +// Multer configuration for image uploads +const upload = multer({ + dest: PICS_PATH, + limits: { fileSize: 5 * 1024 * 1024 }, // 5MB + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only images allowed')); + } + }, + filename: (req, file, cb) => { + cb(null, file.originalname); + } +}); + +// Utility functions +const readData = async () => { + const data = await fs.readFile(DATA_PATH, 'utf8'); + return JSON.parse(data); +}; + +const writeData = async (data) => { + await fs.writeFile(DATA_PATH, JSON.stringify(data, null, 2)); +}; + +const validateQuestion = (q) => { + return q.description && q.a && q.b && q.c && q.d && + q.correct >= 1 && q.correct <= 4 && + ['md', 'me', 'mf'].includes(q.type) && q.source; +}; + +// Public routes +app.get('/api/fizika', async (req, res) => { + try { + const data = await readData(); + res.json(data); + } catch (error) { + res.status(500).json({ error: 'Failed to read data' }); + } +}); + +app.get('/api/images', async (req, res) => { + try { + const files = await fs.readdir(PICS_PATH); + const images = files.filter(f => /\.(jpg|jpeg|png|gif|bmp)$/i.test(f)); + res.json(images); + } catch (error) { + res.status(500).json({ error: 'Failed to read images' }); + } +}); + +app.use('/api/pics', express.static(PICS_PATH)); + +// Admin routes (no auth required) +app.get('/api/admin/questions', async (req, res) => { + try { + const data = await readData(); + res.json(data); + } catch (error) { + res.status(500).json({ error: 'Failed to read questions' }); + } +}); + +app.post('/api/admin/questions', async (req, res) => { + try { + if (!validateQuestion(req.body)) { + return res.status(400).json({ error: 'Invalid question data' }); + } + + const data = await readData(); + const maxId = Math.max(...data.map(q => q.id), 0); + + const newQuestion = { id: maxId + 1, ...req.body }; + data.push(newQuestion); + await writeData(data); + + res.status(201).json(newQuestion); + } catch (error) { + res.status(500).json({ error: 'Failed to create question' }); + } +}); + +app.put('/api/admin/questions/:id', async (req, res) => { + try { + if (!validateQuestion(req.body)) { + return res.status(400).json({ error: 'Invalid question data' }); + } + + const data = await readData(); + const index = data.findIndex(q => q.id === parseInt(req.params.id)); + + if (index === -1) { + return res.status(404).json({ error: 'Question not found' }); + } + + data[index] = { ...data[index], ...req.body }; + await writeData(data); + + res.json(data[index]); + } catch (error) { + res.status(500).json({ error: 'Failed to update question' }); + } +}); + +app.delete('/api/admin/questions/:id', async (req, res) => { + try { + const data = await readData(); + const index = data.findIndex(q => q.id === parseInt(req.params.id)); + + if (index === -1) { + return res.status(404).json({ error: 'Question not found' }); + } + + data.splice(index, 1); + await writeData(data); + + res.json({ message: 'Question deleted' }); + } catch (error) { + res.status(500).json({ error: 'Failed to delete question' }); + } +}); + +app.post('/api/admin/images/upload', upload.single('image'), (req, res) => { + if (!req.file) { + return res.status(400).json({ error: 'No image provided' }); + } + + res.json({ + filename: req.file.filename, + path: `/api/pics/${req.file.filename}` + }); +}); + +app.delete('/api/admin/images/:filename', async (req, res) => { + try { + await fs.unlink(path.join(PICS_PATH, req.params.filename)); + res.json({ message: 'Image deleted' }); + } catch (error) { + if (error.code === 'ENOENT') { + return res.status(404).json({ error: 'Image not found' }); + } + res.status(500).json({ error: 'Failed to delete image' }); + } +}); + +// Error handling +app.use((error, req, res, next) => { + console.error('Error:', error.message); + res.status(500).json({ error: 'Server error' }); +}); + +app.use((req, res) => { + res.status(404).json({ error: 'Not found' }); +}); + +app.listen(PORT, () => { + console.log(`Fizika Admin Backend running on port ${PORT}`); +}); \ No newline at end of file diff --git a/auxIMG/bg.jpg b/frontend/auxIMG/bg.jpg similarity index 100% rename from auxIMG/bg.jpg rename to frontend/auxIMG/bg.jpg diff --git a/auxIMG/checked.svg b/frontend/auxIMG/checked.svg similarity index 100% rename from auxIMG/checked.svg rename to frontend/auxIMG/checked.svg diff --git a/auxIMG/load.gif b/frontend/auxIMG/load.gif similarity index 100% rename from auxIMG/load.gif rename to frontend/auxIMG/load.gif diff --git a/auxIMG/unchecked.svg b/frontend/auxIMG/unchecked.svg similarity index 100% rename from auxIMG/unchecked.svg rename to frontend/auxIMG/unchecked.svg diff --git a/css/button.css b/frontend/css/button.css similarity index 100% rename from css/button.css rename to frontend/css/button.css diff --git a/css/card.css b/frontend/css/card.css similarity index 100% rename from css/card.css rename to frontend/css/card.css diff --git a/css/fizika.css b/frontend/css/fizika.css similarity index 100% rename from css/fizika.css rename to frontend/css/fizika.css diff --git a/css/radio.css b/frontend/css/radio.css similarity index 100% rename from css/radio.css rename to frontend/css/radio.css diff --git a/favicons/android-chrome-192x192.png b/frontend/favicons/android-chrome-192x192.png similarity index 100% rename from favicons/android-chrome-192x192.png rename to frontend/favicons/android-chrome-192x192.png diff --git a/favicons/android-chrome-384x384.png b/frontend/favicons/android-chrome-384x384.png similarity index 100% rename from favicons/android-chrome-384x384.png rename to frontend/favicons/android-chrome-384x384.png diff --git a/favicons/apple-touch-icon-120x120.png b/frontend/favicons/apple-touch-icon-120x120.png similarity index 100% rename from favicons/apple-touch-icon-120x120.png rename to frontend/favicons/apple-touch-icon-120x120.png diff --git a/favicons/apple-touch-icon-152x152.png b/frontend/favicons/apple-touch-icon-152x152.png similarity index 100% rename from favicons/apple-touch-icon-152x152.png rename to frontend/favicons/apple-touch-icon-152x152.png diff --git a/favicons/apple-touch-icon-180x180.png b/frontend/favicons/apple-touch-icon-180x180.png similarity index 100% rename from favicons/apple-touch-icon-180x180.png rename to frontend/favicons/apple-touch-icon-180x180.png diff --git a/favicons/apple-touch-icon-60x60.png b/frontend/favicons/apple-touch-icon-60x60.png similarity index 100% rename from favicons/apple-touch-icon-60x60.png rename to frontend/favicons/apple-touch-icon-60x60.png diff --git a/favicons/apple-touch-icon-76x76.png b/frontend/favicons/apple-touch-icon-76x76.png similarity index 100% rename from favicons/apple-touch-icon-76x76.png rename to frontend/favicons/apple-touch-icon-76x76.png diff --git a/favicons/apple-touch-icon.png b/frontend/favicons/apple-touch-icon.png similarity index 100% rename from favicons/apple-touch-icon.png rename to frontend/favicons/apple-touch-icon.png diff --git a/favicons/favicon-16x16.png b/frontend/favicons/favicon-16x16.png similarity index 100% rename from favicons/favicon-16x16.png rename to frontend/favicons/favicon-16x16.png diff --git a/favicons/favicon-32x32.png b/frontend/favicons/favicon-32x32.png similarity index 100% rename from favicons/favicon-32x32.png rename to frontend/favicons/favicon-32x32.png diff --git a/favicons/favicon.ico b/frontend/favicons/favicon.ico similarity index 100% rename from favicons/favicon.ico rename to frontend/favicons/favicon.ico diff --git a/favicons/manifest.json b/frontend/favicons/manifest.json similarity index 100% rename from favicons/manifest.json rename to frontend/favicons/manifest.json diff --git a/fizika.json b/frontend/fizika.json similarity index 100% rename from fizika.json rename to frontend/fizika.json diff --git a/index.html b/frontend/index.html similarity index 100% rename from index.html rename to frontend/index.html diff --git a/js/fizika.js b/frontend/js/fizika.js similarity index 100% rename from js/fizika.js rename to frontend/js/fizika.js diff --git a/js/jquery.min.js b/frontend/js/jquery.min.js similarity index 100% rename from js/jquery.min.js rename to frontend/js/jquery.min.js diff --git a/js/load.js b/frontend/js/load.js similarity index 84% rename from js/load.js rename to frontend/js/load.js index e2b3f50..a068f4b 100644 --- a/js/load.js +++ b/frontend/js/load.js @@ -1,4 +1,6 @@ let questions = null; +const API_BASE = window.location.hostname === 'localhost' ? 'http://localhost:3001' : 'https://your-backend-domain.com'; + const loadQuestions = async ( isSearch, categories, @@ -6,7 +8,12 @@ const loadQuestions = async ( questionCount ) => { if (questions === null) { - questions = await (await fetch("fizika.json")).json(); + try { + questions = await (await fetch(`${API_BASE}/api/fizika`)).json(); + } catch (error) { + console.error('Failed to load questions from API, falling back to local file:', error); + questions = await (await fetch("fizika.json")).json(); + } } let currentQuestions = questions.slice(); @@ -31,7 +38,7 @@ const loadQuestions = async (

${i + 1}.

${source}

${description}
- ${image ? `
` : ""} + ${image ? `
` : ""}