diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml deleted file mode 100644 index cee1627..0000000 --- a/.forgejo/workflows/deploy.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Deploy to Pages - -on: - push: - branches: ['main'] - pull_request: - branches: ['main'] - workflow_dispatch: - -concurrency: - group: 'pages' - cancel-in-progress: false - -jobs: - deploy: - runs-on: docker - - steps: - - uses: actions/checkout@v4 - - - name: Validate static frontend - run: | - test -f frontend/index.html - test -f frontend/fizika.json - - - name: Copy frontend to host pages mount - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: | - apt update && apt install -y rsync - mkdir -p /pages - rsync -a --delete frontend/ /pages/fizika diff --git a/.forgejo/workflows/docker-publish.yml b/.forgejo/workflows/docker-publish.yml deleted file mode 100644 index f13528a..0000000 --- a/.forgejo/workflows/docker-publish.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Build and Publish Docker Image - -on: - push: - branches: ['main'] - tags: ['v*'] - pull_request: - branches: ['main'] - workflow_dispatch: - -env: - IMAGE_NAME: ${{ forgejo.repository }}/fizika-admin - -jobs: - build-and-push: - runs-on: ubuntu-docker - - steps: - - name: Checkout repository - uses: https://code.forgejo.org/actions/checkout@v4 - - - name: Extract registry host - id: registry - run: echo "host=$(echo '${{ forgejo.server_url }}' | sed 's|https\?://||')" >> "$GITHUB_OUTPUT" - - - name: Log into Forgejo registry - if: forgejo.event_name != 'pull_request' - run: echo "${{ secrets.FORGEJO_TOKEN }}" | docker login "${{ steps.registry.outputs.host }}" -u "${{ forgejo.actor }}" --password-stdin - - - name: Build Docker image - run: | - IMAGE="${{ steps.registry.outputs.host }}/$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" - SHA_SHORT="$(echo "${{ forgejo.sha }}" | cut -c1-12)" - TAG_ARGS="-t ${IMAGE}:sha-${SHA_SHORT}" - - if [ "${{ forgejo.ref }}" = "refs/heads/main" ]; then - TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:main -t ${IMAGE}:latest" - fi - - if [ "${{ forgejo.ref_type }}" = "tag" ]; then - REF_NAME="${{ forgejo.ref_name }}" - TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:${REF_NAME}" - - if echo "$REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then - VERSION="${REF_NAME#v}" - MAJOR_MINOR="$(echo "$VERSION" | cut -d. -f1,2)" - MAJOR="$(echo "$VERSION" | cut -d. -f1)" - TAG_ARGS="${TAG_ARGS} -t ${IMAGE}:${VERSION} -t ${IMAGE}:${MAJOR_MINOR} -t ${IMAGE}:${MAJOR}" - fi - fi - - docker build \ - --label "org.opencontainers.image.source=${{ forgejo.server_url }}/${{ forgejo.repository }}" \ - --label "org.opencontainers.image.revision=${{ forgejo.sha }}" \ - --label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ - ${TAG_ARGS} \ - ./backend - - echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV" - - - name: Push Docker image - if: forgejo.event_name != 'pull_request' - run: docker push --all-tags "$IMAGE" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1230149 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..f4851b4 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,36 @@ +name: Deploy to Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "frontend" + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..351e43e --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,86 @@ +name: Build and Publish Docker Image + +on: + push: + branches: ["main"] + +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. + # 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: Sign the published Docker image + if: ${{ github.ref_type == 'tag' }} + 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} diff --git a/README.md b/README.md index 909cef3..1bb4f69 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# Fizika +# 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 diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md new file mode 100644 index 0000000..124ed9c --- /dev/null +++ b/backend/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Development Commands + +### Running the Application +```bash +npm run dev # Development mode with nodemon hot reload +npm start # Production mode +``` + +### Docker Commands +```bash +# Production deployment +docker-compose up -d + +# Development with hot reload +docker-compose --profile dev up fizika-admin-dev + +# Build image manually +docker build -t fizika-admin ./backend +``` + +### Environment Setup +```bash +cp .env.example .env +# Edit .env with your configuration +``` + +## Architecture Overview + +This is a single-file Node.js/Express backend (`server.js`) that serves as both: +1. **API Server**: Provides REST endpoints for question and image management +2. **Static File Server**: Serves the admin interface from `/public/` directory + +### Key Components + +**Data Storage**: +- Questions stored in JSON file at `../frontend/fizika.json` (configurable via `DATA_PATH`) +- Images stored in `../frontend/pics/` directory (configurable via `PICS_PATH`) + +**Admin Interface**: +- Built-in web UI served from `/public/index.html` +- JavaScript client in `/public/admin.js` +- No authentication required (simplified for admin use) + +**API Structure**: +- Public endpoints: `/api/fizika`, `/api/images`, `/api/pics/:filename` +- Admin endpoints: `/api/admin/questions`, `/api/admin/images` +- No JWT authentication implemented despite README documentation + +### Question Data 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" +} +``` + +### Complete Question Types (17 Categories) + +**IMPORTANT**: The README only mentions 3 types, but the frontend supports all 17: + +**Mechanics (Mechanika)**: +- `mec` - Mechanika (general mechanics) +- `mk` - Kinematika (kinematics) +- `md` - Dinamika (dynamics) +- `me` - Munka és energia (work and energy) +- `mf` - Folyadékok és gázok mechanikája (fluid mechanics) +- `mr` - Rezgések és hullámok (oscillations and waves) + +**Thermodynamics**: +- `h` - Hőtan (thermodynamics) + +**Electricity**: +- `ele` - Elektromosság (general electricity) +- `es` - Elektrosztatika (electrostatics) +- `ee` - Egyenáram (direct current) +- `ev` - Váltakozó áram (alternating current) + +**Other Physics**: +- `m` - Mágnesesség (magnetism) +- `o` - Fénytan (optics) +- `atm` - Atomfizika (general atomic physics) +- `ah` - Atomhéj (electron shells) +- `am` - Atommag (atomic nucleus) +- `cs` - Égi mechanika, csillagászat (celestial mechanics, astronomy) +- `v` - Vegyes (mixed/various) + +### Security Considerations +- Helmet.js for security headers +- CORS configuration via `FRONTEND_URL` environment variable +- File upload restricted to images only (5MB limit) +- Input validation minimal - add validation when modifying endpoints + +## File Structure +``` +backend/ +├── server.js # Main application file +├── public/ +│ ├── index.html # Admin interface HTML +│ └── admin.js # Admin interface JavaScript +├── package.json +├── Dockerfile +├── docker-compose.yml +└── .env.example +``` + +## Important Notes + +- **Single File Architecture**: All server logic is in `server.js` - no separate route files or modules +- **File-based Data**: Uses JSON file for persistence, not a database +- **No Authentication**: Despite README documentation mentioning JWT, no auth is implemented +- **Path Dependencies**: Assumes frontend directory structure (`../frontend/fizika.json`, `../frontend/pics/`) +- **Admin UI Included**: Built-in web interface accessible at root path `/` +- **Question Types**: Support all 17 physics categories listed above, not just the 3 in README + +## Making Changes + +When modifying the API: +1. All changes go in `server.js` +2. Test both API endpoints and admin UI functionality +3. Ensure question type validation supports all 17 categories if adding validation +4. Consider impact on file paths and data format +5. Update environment variables in `.env.example` if needed \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 4df2b9e..e267374 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -17,6 +17,7 @@ ENV NODE_ENV=production ENV PORT=3001 EXPOSE 3001 +# 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))" diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..a72505e --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,70 @@ +version: '3.8' + +services: + fizika-admin: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "3001:3001" + environment: + - NODE_ENV=production + - PORT=3001 + - FRONTEND_URL=${FRONTEND_URL:-*} + - DATA_PATH=${DATA_PATH:-/usr/src/app/fizika.json} + - PICS_PATH=${PICS_PATH:-/usr/src/app/pics} + 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=* + - DATA_PATH=/usr/src/app/fizika.json + - PICS_PATH=/usr/src/app/pics + 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/public/admin.js b/backend/public/admin.js index 80b2086..4739746 100644 --- a/backend/public/admin.js +++ b/backend/public/admin.js @@ -1,11 +1,5 @@ const API_BASE = window.location.origin; -window.plausible = - window.plausible || - function () { - (window.plausible.q = window.plausible.q || []).push(arguments); - }; - document.addEventListener("DOMContentLoaded", function () { loadQuestions(); loadImages(); diff --git a/backend/public/index.html b/backend/public/index.html index 8dd795a..068322b 100644 --- a/backend/public/index.html +++ b/backend/public/index.html @@ -10,6 +10,13 @@ data-api="https://stats.schmelczer.dev/status" src="https://stats.schmelczer.dev/js/script.file-downloads.hash.outbound-links.js" > +