diff --git a/Frontend/1 het/backend/README.md b/Frontend/1 het/backend/README.md index 37c37e5..3df55c3 100644 --- a/Frontend/1 het/backend/README.md +++ b/Frontend/1 het/backend/README.md @@ -62,8 +62,15 @@ Cookie is HTTP-only and set with `sameSite=lax`. - `GET /api/health` - `GET /api/shop/categories` - `GET /api/shop/products` +- `GET /api/shop/products/:productId` +- `POST /api/shop/products` (`multipart/form-data` is supported, field name: `image`) +- `POST /api/shop/products/:productId/image` (`multipart/form-data`, field name: `image`) - `POST /api/shop/orders` +Uploaded files are stored in `images/uploads` and are served at `/images/uploads/`. + +Product responses include `image_url` with an absolute URL when an image exists. + Order payload: ```json diff --git a/Frontend/1 het/backend/STUDENT_SETUP.md b/Frontend/1 het/backend/STUDENT_SETUP.md new file mode 100644 index 0000000..b80b7a6 --- /dev/null +++ b/Frontend/1 het/backend/STUDENT_SETUP.md @@ -0,0 +1,214 @@ +# Student Production Setup Guide + +This guide explains how to set up and run the webstore backend in production mode. + +## What You'll Receive + +Your instructor will provide you with: + +1. **webstore-production-images.tar** - Docker images archive (API + database) +2. **production.compose.yml** - Docker Compose configuration +3. **.production.env** - Environment variables template +4. **run-student-production.bat** - Easy start script +5. **Product images** - 17 placeholder product images + +## Prerequisites + +- **Docker Desktop** installed ([Download here](https://www.docker.com/products/docker-desktop)) +- Windows 10 or later (for Docker) +- Minimum 2GB free disk space +- ~5 minutes for initial setup + +## Step-by-Step Setup + +### Step 1: Extract Docker Images + +``` +Right-click on webstore-production-images.tar +→ Select "Extract All" +→ Choose destination folder +``` + +Or use PowerShell/terminal: +```powershell +docker load -i webstore-production-images.tar +``` + +### Step 2: Configure Environment Variables + +1. Open `.production.env` in a text editor +2. Update these values (if different from defaults): + +```env +# Database Configuration +POSTGRES_DB=webstore_db +POSTGRES_USER=webstore_user +POSTGRES_PASSWORD=webstore_password +DB_PORT=5432 + +# API Configuration +PORT=3000 +API_PORT=3000 + +# JWT Configuration +JWT_SECRET=your-secret-key-change-for-production +JWT_EXPIRES_IN=7d + +# Security +COOKIE_NAME=webstore_token +COOKIE_SECURE=false # Set to true if using HTTPS + +# Database URL (usually don't change this) +DATABASE_URL=postgresql://webstore_user:webstore_password@db:5432/webstore_db?schema=public +``` + +### Step 3: Start the Application + +**Option A: Using the provided script (recommended)** +``` +Double-click: run-student-production.bat +``` + +**Option B: Using Docker Compose directly** +```powershell +docker-compose -f production.compose.yml up +``` + +### Step 4: Verify Setup + +Wait for logs to show: +``` +webstore_api_prod | npm notice +webstore_api_prod | > webstore-backend@1.0.0 start +webstore_api_prod | > node src/Api/server.js +webstore_api_prod | Server running on port 3000 +``` + +### Step 5: Test the API + +Open in your browser: +``` +http://localhost:3000/api/health +``` + +Expected response: +```json +{ + "status": "ok", + "message": "CQRS backend running" +} +``` + +## Accessing the API + +The API is available at: `http://localhost:3000` + +### Key Endpoints + +- `GET /api/health` - Health check +- `GET /api/shop/categories` - List all product categories +- `GET /api/shop/products` - List all products +- `GET /api/shop/products/:id` - Get product by ID +- `POST /api/shop/products` - Create product (with image upload) +- `POST /api/shop/products/:id/image` - Update product image + +### Built-in Data + +The database automatically seeds with: + +**Test Users:** +``` +Email: admin@test.com +Password: admin123 + +Email: john@test.com +Password: password123 + +Email: jane@test.com +Password: password123 +``` + +**Products:** +- 16 sample products across 5 categories +- Each product has a placeholder image +- Sample prices range from 4,990 to 29,990 HUF + +**Categories:** +- Shoes (4 products) +- Bags (4 products) +- Accessories (4 products) +- Clothing (3 products) +- Electronics (2 products) + +## Stopping the Application + +### Using Docker Compose: +```powershell +docker-compose -f production.compose.yml down +``` + +### Or simply close the terminal window + +## Troubleshooting + +### Port Already in Use +If you get "port 3000 already in use": +```powershell +# Change port in .production.env +API_PORT=3001 +``` + +### Database Connection Failed +- Verify database container is running: `docker ps` +- Check DATABASE_URL in `.production.env` +- Ensure PostgreSQL image is loaded: `docker images | grep postgres` + +### Docker Not Running +- Start Docker Desktop application +- Wait for Docker daemon to be ready (check system tray) + +### Out of Disk Space +```powershell +# Clean up Docker +docker system prune -a +``` + +## Image Upload + +Once running, you can upload product images: + +```bash +curl -X POST http://localhost:3000/api/shop/products/1/image \ + -F "image=@path/to/image.jpg" +``` + +Images are stored in the `images/uploads/` directory and served automatically. + +## Database Persistence + +Product data is stored in PostgreSQL. Data persists between application restarts unless you: +```powershell +docker-compose -f production.compose.yml down -v +``` + +The `-v` flag removes data volumes (use carefully!). + +## Connecting Frontend + +Configure your frontend to use: +``` +API Base URL: http://localhost:3000/api +``` + +Example (fetch): +```javascript +const response = await fetch('http://localhost:3000/api/shop/products'); +const data = await response.json(); +console.log(data); +``` + +## Questions or Issues? + +Consult your instructor or check the main README.md for more information about the backend architecture and CQRS pattern. + +Happy coding! 🚀 diff --git a/Frontend/1 het/backend/images/README.md b/Frontend/1 het/backend/images/README.md new file mode 100644 index 0000000..5e9adb2 --- /dev/null +++ b/Frontend/1 het/backend/images/README.md @@ -0,0 +1,104 @@ +# Product Images Directory + +This directory contains product images for the webstore backend. + +## Image Requirements + +The following 17 placeholder images are required for the seed data to display properly: + +**Shoes (4 images)** +- `street-runner.jpg` - 18,990 HUF +- `trail-edge.jpg` - 24,990 HUF +- `urban-sprint.jpg` - 22,990 HUF +- `classic-comfort.jpg` - 16,990 HUF + +**Bags (4 images)** +- `urban-tote.jpg` - 14,990 HUF +- `backpack-pro.jpg` - 19,990 HUF +- `crossbody.jpg` - 9,990 HUF +- `duffle.jpg` - 24,990 HUF + +**Accessories (4 images)** +- `classic-cap.jpg` - 6,990 HUF +- `silk-scarf.jpg` - 12,990 HUF +- `leather-belt.jpg` - 8,990 HUF +- `watch-straps.jpg` - 4,990 HUF + +**Clothing (3 images)** +- `cotton-tshirt.jpg` - 7,990 HUF +- `denim-jacket.jpg` - 29,990 HUF +- `yoga-leggings.jpg` - 11,990 HUF + +**Electronics (2 images)** +- `earbuds.jpg` - 17,990 HUF +- `usb-hub.jpg` - 5,990 HUF + +## Directory Structure + +``` +images/ +├── [17 product placeholder images] +├── uploads/ (auto-created by multer) +│ └── [user-uploaded images] +├── production.compose.yml (production docker-compose) +├── .production.env (production environment variables) +└── run-student-production.bat (production start script) +``` + +## Image Specifications + +**Recommended specifications for product images:** +- Format: JPEG or PNG +- Dimensions: 400-600px width, 300-600px height +- File size: < 500KB per image +- Color space: RGB or RGBA + +## Uploads Directory + +The `uploads/` directory is automatically created by the multer middleware when the backend starts. This directory stores images uploaded by users through the frontend. + +**Important notes:** +- Images are stored with timestamps: `1717123456789-product-name.jpg` +- Served via: `GET /images/uploads/{filename}` +- Max file size: 5MB per upload +- Only image MIME types allowed + +## Production Build + +When building for production with `build-production-images.bat`: +1. ✓ Placeholder images are automatically generated (if ImageMagick or PowerShell available) +2. ✓ Seeding runs automatically when containers start +3. ✓ Upload directory is created on first run + +## Database Seeding + +The database automatically seeds with: +- **5 categories**: Shoes, Bags, Accessories, Clothing, Electronics +- **16 products**: 2-4 per category with placeholder images +- **3 test users**: admin@test.com, john@test.com, jane@test.com + +## Customization + +To use custom product images: + +1. **Development**: Replace placeholder images in this directory +2. **Production**: Include custom images in the `images/` directory before building +3. **After deployment**: Use the API to upload images via `POST /api/shop/products/{id}/image` + +## Troubleshooting + +### Images not displaying in frontend +- Ensure images are placed in the correct directory +- Check file names match exactly (case-sensitive on Linux/Mac) +- Verify `express.static` is configured in `src/Api/app.js` +- Check console for 404 errors + +### Upload fails +- Check file is an image (JPEG, PNG, WebP, GIF, etc.) +- Verify file size < 5MB +- Ensure `uploads/` directory exists and is writable + +### Seeding without images +- Seed will complete successfully even without images +- Products will have `null` imageUrl values +- Frontend can display placeholder or error images diff --git a/Frontend/1 het/backend/images/backpack-pro.jpg b/Frontend/1 het/backend/images/backpack-pro.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/backpack-pro.jpg differ diff --git a/Frontend/1 het/backend/images/classic-cap.jpg b/Frontend/1 het/backend/images/classic-cap.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/classic-cap.jpg differ diff --git a/Frontend/1 het/backend/images/classic-comfort.jpg b/Frontend/1 het/backend/images/classic-comfort.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/classic-comfort.jpg differ diff --git a/Frontend/1 het/backend/images/cotton-tshirt.jpg b/Frontend/1 het/backend/images/cotton-tshirt.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/cotton-tshirt.jpg differ diff --git a/Frontend/1 het/backend/images/crossbody.jpg b/Frontend/1 het/backend/images/crossbody.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/crossbody.jpg differ diff --git a/Frontend/1 het/backend/images/denim-jacket.jpg b/Frontend/1 het/backend/images/denim-jacket.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/denim-jacket.jpg differ diff --git a/Frontend/1 het/backend/images/duffle.jpg b/Frontend/1 het/backend/images/duffle.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/duffle.jpg differ diff --git a/Frontend/1 het/backend/images/earbuds.jpg b/Frontend/1 het/backend/images/earbuds.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/earbuds.jpg differ diff --git a/Frontend/1 het/backend/images/leather-belt.jpg b/Frontend/1 het/backend/images/leather-belt.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/leather-belt.jpg differ diff --git a/Frontend/1 het/backend/images/run-student-production.bat b/Frontend/1 het/backend/images/run-student-production.bat index 66647ed..0f327ac 100644 --- a/Frontend/1 het/backend/images/run-student-production.bat +++ b/Frontend/1 het/backend/images/run-student-production.bat @@ -4,7 +4,7 @@ setlocal EnableExtensions EnableDelayedExpansion set "ROOT_DIR=%~dp0" set "ENV_FILE=%ROOT_DIR%\.production.env" set "COMPOSE_FILE=%ROOT_DIR%\production.compose.yml" -set "ARCHIVE_FILE=%ROOT_DIR%\images\webstore-production-images.tar" +set "ARCHIVE_FILE=%ROOT_DIR%\webstore-production-images.tar" set "API_IMAGE=webstore-api:prod" diff --git a/Frontend/1 het/backend/images/silk-scarf.jpg b/Frontend/1 het/backend/images/silk-scarf.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/silk-scarf.jpg differ diff --git a/Frontend/1 het/backend/images/street-runner.jpg b/Frontend/1 het/backend/images/street-runner.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/street-runner.jpg differ diff --git a/Frontend/1 het/backend/images/trail-edge.jpg b/Frontend/1 het/backend/images/trail-edge.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/trail-edge.jpg differ diff --git a/Frontend/1 het/backend/images/urban-sprint.jpg b/Frontend/1 het/backend/images/urban-sprint.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/urban-sprint.jpg differ diff --git a/Frontend/1 het/backend/images/urban-tote.jpg b/Frontend/1 het/backend/images/urban-tote.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/urban-tote.jpg differ diff --git a/Frontend/1 het/backend/images/usb-hub.jpg b/Frontend/1 het/backend/images/usb-hub.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/usb-hub.jpg differ diff --git a/Frontend/1 het/backend/images/watch-straps.jpg b/Frontend/1 het/backend/images/watch-straps.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/watch-straps.jpg differ diff --git a/Frontend/1 het/backend/images/webstore-production-images.tar b/Frontend/1 het/backend/images/webstore-production-images.tar index 30e0b90..70aa470 100644 Binary files a/Frontend/1 het/backend/images/webstore-production-images.tar and b/Frontend/1 het/backend/images/webstore-production-images.tar differ diff --git a/Frontend/1 het/backend/images/yoga-leggings.jpg b/Frontend/1 het/backend/images/yoga-leggings.jpg new file mode 100644 index 0000000..9ada010 Binary files /dev/null and b/Frontend/1 het/backend/images/yoga-leggings.jpg differ diff --git a/Frontend/1 het/backend/package-lock.json b/Frontend/1 het/backend/package-lock.json index 0394b19..ac512b4 100644 --- a/Frontend/1 het/backend/package-lock.json +++ b/Frontend/1 het/backend/package-lock.json @@ -18,7 +18,8 @@ "express": "^4.19.2", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "multer": "^2.1.1" }, "devDependencies": { "nodemon": "^3.1.4", @@ -120,6 +121,12 @@ "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", @@ -229,6 +236,23 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "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", @@ -292,6 +316,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.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", @@ -1020,6 +1059,25 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1242,6 +1300,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1438,6 +1510,23 @@ "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.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1496,6 +1585,12 @@ "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", @@ -1512,6 +1607,12 @@ "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", diff --git a/Frontend/1 het/backend/package.json b/Frontend/1 het/backend/package.json index 1277912..89dc30a 100644 --- a/Frontend/1 het/backend/package.json +++ b/Frontend/1 het/backend/package.json @@ -9,6 +9,8 @@ "prisma:generate": "prisma generate", "prisma:push": "prisma db push", "prisma:seed": "node prisma/seed.js", + "generate-data": "node scripts/generate-test-data.js", + "generate-images": "node scripts/generate-placeholder-images.js", "postinstall": "prisma generate" }, "keywords": [ @@ -20,6 +22,7 @@ "author": "", "license": "MIT", "dependencies": { + "@prisma/client": "^5.20.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -28,10 +31,10 @@ "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", - "@prisma/client": "^5.20.0" + "multer": "^2.1.1" }, "devDependencies": { "nodemon": "^3.1.4", "prisma": "^5.20.0" } -} \ No newline at end of file +} diff --git a/Frontend/1 het/backend/prisma/seed.js b/Frontend/1 het/backend/prisma/seed.js index 15ca131..495d856 100644 --- a/Frontend/1 het/backend/prisma/seed.js +++ b/Frontend/1 het/backend/prisma/seed.js @@ -1,14 +1,21 @@ const { PrismaClient } = require("@prisma/client"); +const bcryptjs = require("bcryptjs"); const prisma = new PrismaClient(); async function main() { + console.log("Starting database seeding..."); + + // Seed categories const categories = [ { name: "Shoes", slug: "shoes" }, { name: "Bags", slug: "bags" }, - { name: "Accessories", slug: "accessories" } + { name: "Accessories", slug: "accessories" }, + { name: "Clothing", slug: "clothing" }, + { name: "Electronics", slug: "electronics" } ]; + console.log("Creating categories..."); for (const category of categories) { await prisma.category.upsert({ where: { slug: category.slug }, @@ -20,12 +27,15 @@ async function main() { const shoes = await prisma.category.findUnique({ where: { slug: "shoes" } }); const bags = await prisma.category.findUnique({ where: { slug: "bags" } }); const accessories = await prisma.category.findUnique({ where: { slug: "accessories" } }); + const clothing = await prisma.category.findUnique({ where: { slug: "clothing" } }); + const electronics = await prisma.category.findUnique({ where: { slug: "electronics" } }); const products = [ + // Shoes { categoryId: shoes.id, name: "Street Runner", - description: "Lightweight city sneaker.", + description: "Lightweight city sneaker perfect for daily commute.", price: "18990.00", imageUrl: "/images/street-runner.jpg", stock: 24 @@ -33,29 +43,138 @@ async function main() { { categoryId: shoes.id, name: "Trail Edge", - description: "Stable shoe for outdoor tracks.", + description: "Stable shoe for outdoor tracks and hiking adventures.", price: "24990.00", imageUrl: "/images/trail-edge.jpg", stock: 13 }, + { + categoryId: shoes.id, + name: "Urban Sprint", + description: "High-performance running shoe for athletes.", + price: "22990.00", + imageUrl: "/images/urban-sprint.jpg", + stock: 18 + }, + { + categoryId: shoes.id, + name: "Classic Comfort", + description: "Timeless design with ultimate comfort for all-day wear.", + price: "16990.00", + imageUrl: "/images/classic-comfort.jpg", + stock: 31 + }, + // Bags { categoryId: bags.id, name: "Urban Tote", - description: "Everyday tote with zipper top.", + description: "Everyday tote with zipper top and multiple compartments.", price: "14990.00", imageUrl: "/images/urban-tote.jpg", stock: 30 }, + { + categoryId: bags.id, + name: "Backpack Pro", + description: "Professional backpack with laptop compartment.", + price: "19990.00", + imageUrl: "/images/backpack-pro.jpg", + stock: 15 + }, + { + categoryId: bags.id, + name: "Crossbody Essentials", + description: "Compact crossbody bag perfect for quick trips.", + price: "9990.00", + imageUrl: "/images/crossbody.jpg", + stock: 45 + }, + { + categoryId: bags.id, + name: "Weekend Duffle", + description: "Spacious duffle bag for travel and weekend getaways.", + price: "24990.00", + imageUrl: "/images/duffle.jpg", + stock: 8 + }, + // Accessories { categoryId: accessories.id, name: "Classic Cap", - description: "Adjustable cotton cap.", + description: "Adjustable cotton cap with curved visor.", price: "6990.00", imageUrl: "/images/classic-cap.jpg", stock: 42 + }, + { + categoryId: accessories.id, + name: "Silk Scarf", + description: "Premium silk scarf with elegant patterns.", + price: "12990.00", + imageUrl: "/images/silk-scarf.jpg", + stock: 20 + }, + { + categoryId: accessories.id, + name: "Leather Belt", + description: "Genuine leather belt with metal buckle.", + price: "8990.00", + imageUrl: "/images/leather-belt.jpg", + stock: 35 + }, + { + categoryId: accessories.id, + name: "Watch Straps Set", + description: "Set of 3 interchangeable watch straps.", + price: "4990.00", + imageUrl: "/images/watch-straps.jpg", + stock: 60 + }, + // Clothing + { + categoryId: clothing.id, + name: "Cotton T-Shirt", + description: "100% organic cotton comfortable t-shirt.", + price: "7990.00", + imageUrl: "/images/cotton-tshirt.jpg", + stock: 50 + }, + { + categoryId: clothing.id, + name: "Denim Jacket", + description: "Classic blue denim jacket perfect for any season.", + price: "29990.00", + imageUrl: "/images/denim-jacket.jpg", + stock: 12 + }, + { + categoryId: clothing.id, + name: "Yoga Leggings", + description: "High-waisted leggings with optimal breathability.", + price: "11990.00", + imageUrl: "/images/yoga-leggings.jpg", + stock: 25 + }, + // Electronics + { + categoryId: electronics.id, + name: "Wireless Earbuds", + description: "Premium noise-cancelling wireless earbuds.", + price: "17990.00", + imageUrl: "/images/earbuds.jpg", + stock: 22 + }, + { + categoryId: electronics.id, + name: "USB-C Hub", + description: "7-in-1 USB-C hub with multiple ports.", + price: "5990.00", + imageUrl: "/images/usb-hub.jpg", + stock: 40 } ]; + console.log("Creating products..."); for (const product of products) { await prisma.product.upsert({ where: { name: product.name }, @@ -63,15 +182,37 @@ async function main() { create: product }); } + + // Seed test users + console.log("Creating test users..."); + const testUsers = [ + { name: "Admin User", email: "admin@test.com", password: "admin123" }, + { name: "John Doe", email: "john@test.com", password: "password123" }, + { name: "Jane Smith", email: "jane@test.com", password: "password123" } + ]; + + for (const user of testUsers) { + const passwordHash = await bcryptjs.hash(user.password, 10); + await prisma.user.upsert({ + where: { email: user.email }, + update: {}, + create: { + name: user.name, + email: user.email, + passwordHash + } + }); + } + + console.log("Database seeding completed successfully!"); } main() .then(async () => { await prisma.$disconnect(); - console.log("Prisma seed completed."); }) .catch(async (error) => { - console.error(error); + console.error("Seeding error:", error); await prisma.$disconnect(); process.exit(1); }); \ No newline at end of file diff --git a/Frontend/1 het/backend/scripts/README.md b/Frontend/1 het/backend/scripts/README.md new file mode 100644 index 0000000..6d11ad2 --- /dev/null +++ b/Frontend/1 het/backend/scripts/README.md @@ -0,0 +1,179 @@ +# Test Data Generation Scripts + +This directory contains scripts to help manage test data for the webstore backend. + +## Available Scripts + +### 0. Generate Placeholder Images +Creates placeholder JPEG images for all 17 products used in seeding. + +**File:** `generate-placeholder-images.js` + +**What it does:** +- Generates 17 placeholder product images (400x300px) +- Saves images to `../images/` directory +- Creates `uploads/` directory for user-uploaded images + +**Usage:** +```bash +# Using npm script +npm run generate-images + +# Or directly with Node +node scripts/generate-placeholder-images.js +``` + +**When to run:** +- First time setting up development environment +- When `/images` route returns 404 (images not found) +- After cleaning up the images directory + +**Images created (17 total):** +- Shoes: street-runner.jpg, trail-edge.jpg, urban-sprint.jpg, classic-comfort.jpg +- Bags: urban-tote.jpg, backpack-pro.jpg, crossbody.jpg, duffle.jpg +- Accessories: classic-cap.jpg, silk-scarf.jpg, leather-belt.jpg, watch-straps.jpg +- Clothing: cotton-tshirt.jpg, denim-jacket.jpg, yoga-leggings.jpg +- Electronics: earbuds.jpg, usb-hub.jpg + +--- + +### 1. Seed Script (Automatic) +Runs automatically when the backend starts in development mode. + +**File:** `../prisma/seed.js` + +**What it does:** +- Creates 5 product categories (Shoes, Bags, Accessories, Clothing, Electronics) +- Creates 16 sample products across all categories +- Creates 3 test user accounts + +**Test User Credentials:** +``` +Email: admin@test.com +Password: admin123 + +Email: john@test.com +Password: password123 + +Email: jane@test.com +Password: password123 +``` + +--- + +## 2. Generate Test Data Script +Generates additional bulk test data on demand. + +### Usage Options + +#### Option A: Windows Batch File (Recommended for Windows) +```bash +# Generate default test data (10 products, 5 orders) +scripts\generate-test-data.bat + +# Generate 20 products instead of 10 +scripts\generate-test-data.bat --products 20 + +# Generate 20 products and 10 orders +scripts\generate-test-data.bat --products 20 --orders 10 + +# DANGEROUS: Reset database and regenerate all data +scripts\generate-test-data.bat --reset --products 50 --orders 20 +``` + +#### Option B: NPM Script +```bash +# Generate default test data +npm run generate-data + +# With arguments (from project root) +npm run generate-data -- --products 30 --orders 15 +``` + +#### Option C: Direct Node.js +```bash +# From project root +node scripts/generate-test-data.js --products 25 --orders 10 +``` + +### Command-line Options + +| Option | Description | Default | +|--------|-------------|---------| +| `--products N` | Number of additional products to generate | 10 | +| `--orders N` | Number of test orders to generate | 5 | +| `--reset` | ⚠️ **DANGEROUS**: Delete all data first | (not set) | + +### Examples + +```bash +# Quick test: Generate 5 products and 2 orders +scripts\generate-test-data.bat --products 5 --orders 2 + +# Moderate data: 30 products and 10 orders +npm run generate-data -- --products 30 --orders 10 + +# Heavy load test: 100 products and 50 orders +scripts\generate-test-data.bat --products 100 --orders 50 + +# Start fresh: Reset and generate 50 products and 20 orders +scripts\generate-test-data.bat --reset --products 50 --orders 20 +``` + +## What Gets Generated + +### Products +- Random product names with auto-incrementing counters +- Random prices between 5,990 and 24,990 HUF +- Random stock quantities (5-55 units) +- Random category assignment +- Placeholder image URLs + +### Test Users +- 5 additional test user accounts +- Email: `user[1-5]@test.com` +- Password: `password123` + +### Orders +- Random customer names +- Random customer emails +- Random order items (1-3 products per order) +- Automatically calculated total prices + +## Important Notes + +⚠️ **WARNING**: The `--reset` flag will delete: +- All orders and order items +- All products +- All categories +- All users + +Only use `--reset` if you know what you're doing! + +## Troubleshooting + +### "Node.js is not installed or not in PATH" +- Install Node.js from https://nodejs.org/ +- Make sure it's in your system PATH + +### "Database connection error" +- Ensure the database is running +- Check `.env` file has correct `DATABASE_URL` +- Run migrations first: `npm run prisma:push` + +### "Unique constraint failed" errors +- Product names must be unique +- The script skips duplicates automatically +- Use `--reset` to start fresh + +## Docker Compose Integration + +The test data generation automatically runs in docker-compose: +```bash +# Start backend (will seed data automatically) +docker-compose up api + +# The initial data is seeded when the container first starts +``` + +For production, you may want to disable auto-seeding in the `docker-compose.yml` file. diff --git a/Frontend/1 het/backend/scripts/build-production-images.bat b/Frontend/1 het/backend/scripts/build-production-images.bat index 5ea58cf..190acf9 100644 --- a/Frontend/1 het/backend/scripts/build-production-images.bat +++ b/Frontend/1 het/backend/scripts/build-production-images.bat @@ -8,14 +8,14 @@ set "ARCHIVE_FILE=%IMAGE_OUT_DIR%\webstore-production-images.tar" set "API_IMAGE=webstore-api:prod" set "DB_IMAGE=postgres:16-alpine" -echo [1/6] Checking Docker availability... +echo [1/7] Checking Docker availability... docker --version >nul 2>&1 if errorlevel 1 ( echo ERROR: Docker CLI is not available. Install Docker Desktop first. exit /b 1 ) -echo [2/6] Building production API image: %API_IMAGE% +echo [2/7] Building production API image: %API_IMAGE% pushd "%ROOT_DIR%" >nul docker build -t "%API_IMAGE%" -f Dockerfile . if errorlevel 1 ( @@ -25,17 +25,33 @@ if errorlevel 1 ( ) popd >nul -echo [3/6] Pulling database image: %DB_IMAGE% +echo [3/7] Pulling database image: %DB_IMAGE% docker pull "%DB_IMAGE%" if errorlevel 1 ( echo ERROR: Failed to pull database image. exit /b 1 ) -echo [4/6] Preparing image output directory: %IMAGE_OUT_DIR% +echo [4/7] Preparing image output directory: %IMAGE_OUT_DIR% if not exist "%IMAGE_OUT_DIR%" mkdir "%IMAGE_OUT_DIR%" -echo [5/6] Exporting images to archive... +echo [5/7] Creating placeholder product images... +pushd "%ROOT_DIR%" >nul + +REM Create uploads directory +if not exist "%IMAGE_OUT_DIR%\uploads" mkdir "%IMAGE_OUT_DIR%\uploads" + +REM Use Node.js to generate placeholder images reliably +echo Generating placeholder images using Node.js... +node "%ROOT_DIR%\scripts\generate-placeholder-images.js" +if errorlevel 1 ( + echo WARNING: Could not generate placeholder images automatically + echo Note: Images can be added manually to %IMAGE_OUT_DIR% +) + +popd >nul + +echo [6/7] Exporting images to archive... if exist "%ARCHIVE_FILE%" del /f /q "%ARCHIVE_FILE%" docker save -o "%ARCHIVE_FILE%" "%API_IMAGE%" "%DB_IMAGE%" if errorlevel 1 ( @@ -43,7 +59,35 @@ if errorlevel 1 ( exit /b 1 ) -echo [6/6] Done. +echo [7/7] Done. +echo. +echo ======================================== +echo PRODUCTION BUILD COMPLETE +echo ======================================== +echo. echo Created archive: "%ARCHIVE_FILE%" -echo Share this file with students together with production.compose.yml and .production.env. +echo. +echo FILES TO DISTRIBUTE TO STUDENTS: +echo 1. "%ARCHIVE_FILE%" +echo 2. "%IMAGE_OUT_DIR%\production.compose.yml" +echo 3. "%IMAGE_OUT_DIR%\.production.env" (with proper configuration) +echo 4. "%IMAGE_OUT_DIR%\run-student-production.bat" +echo. +echo WHAT'S INCLUDED: +echo - Production API Docker image +echo - PostgreSQL database image +echo - Initial seeding script (creates 5 categories, 16 products, 3 test users) +echo - Placeholder product images (17 images) +echo - Upload directory for student-generated images +echo. +echo STUDENTS SHOULD: +echo 1. Extract webstore-production-images.tar +echo 2. Copy .production.env and configure database credentials +echo 3. Run run-student-production.bat +echo 4. Database will auto-seed with sample data +echo 5. Test users available: +echo - admin@test.com / admin123 +echo - john@test.com / password123 +echo - jane@test.com / password123 +echo. exit /b 0 \ No newline at end of file diff --git a/Frontend/1 het/backend/scripts/generate-placeholder-images.js b/Frontend/1 het/backend/scripts/generate-placeholder-images.js new file mode 100644 index 0000000..a186dd8 --- /dev/null +++ b/Frontend/1 het/backend/scripts/generate-placeholder-images.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +// Image list from README +const images = [ + // Shoes + 'street-runner.jpg', + 'trail-edge.jpg', + 'urban-sprint.jpg', + 'classic-comfort.jpg', + // Bags + 'urban-tote.jpg', + 'backpack-pro.jpg', + 'crossbody.jpg', + 'duffle.jpg', + // Accessories + 'classic-cap.jpg', + 'silk-scarf.jpg', + 'leather-belt.jpg', + 'watch-straps.jpg', + // Clothing + 'cotton-tshirt.jpg', + 'denim-jacket.jpg', + 'yoga-leggings.jpg', + // Electronics + 'earbuds.jpg', + 'usb-hub.jpg', +]; + +const imagesDir = path.join(__dirname, '..', 'images'); +const uploadsDir = path.join(imagesDir, 'uploads'); + +// Create directories if they don't exist +if (!fs.existsSync(imagesDir)) { + fs.mkdirSync(imagesDir, { recursive: true }); + console.log(`Created ${imagesDir}`); +} + +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); + console.log(`Created ${uploadsDir}`); +} + +// Create simple JPEG placeholder images (minimal valid JPEG structure) +// This is a minimal JPEG structure that works with image viewers +const createPlaceholderJPEG = (filename, width, height) => { + // Minimal JPEG header and footer + const jpegHeader = Buffer.from([ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, + 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, + 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, + 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, + 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, + 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, + 0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20, + 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, + 0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, + 0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34, + 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, height, + 0x00, width, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, + 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, + 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, + 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, + 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, + 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, + 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, + 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, + 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, + 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, + 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, + 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, + 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, + 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, + 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, + 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, + 0xFB, 0xD0, 0xFF, 0xD9, + ]); + + return jpegHeader; +}; + +let created = 0; +images.forEach((filename) => { + const filepath = path.join(imagesDir, filename); + if (!fs.existsSync(filepath)) { + const jpegData = createPlaceholderJPEG(filename, 400, 300); + fs.writeFileSync(filepath, jpegData); + console.log(`✓ Created ${filename}`); + created++; + } else { + console.log(`✓ Already exists: ${filename}`); + } +}); + +console.log(`\n${created} placeholder images generated successfully!`); +console.log(`Images directory: ${imagesDir}`); diff --git a/Frontend/1 het/backend/scripts/generate-test-data.bat b/Frontend/1 het/backend/scripts/generate-test-data.bat new file mode 100644 index 0000000..c3ab9c1 --- /dev/null +++ b/Frontend/1 het/backend/scripts/generate-test-data.bat @@ -0,0 +1,87 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +:: +:: Generate test data script for webstore backend +:: Usage: generate-test-data.bat [--products N] [--orders N] [--reset] +:: +:: Examples: +:: generate-test-data.bat (Creates 10 products, 5 orders) +:: generate-test-data.bat --products 20 (Creates 20 products, 5 orders) +:: generate-test-data.bat --products 50 --orders 10 --reset (Resets DB and generates data) +:: + +cd /d "%~dp0.." + +echo. +echo ======================================== +echo Test Data Generator for Webstore +echo ======================================== +echo. + +set "PRODUCTS=10" +set "ORDERS=5" +set "RESET_FLAG=" + +:parse_args +if "%~1"=="" goto start_generation +if "%~1"=="--products" ( + if "%~2"=="" ( + echo ERROR: --products requires a number argument + exit /b 1 + ) + set "PRODUCTS=%~2" + shift + shift + goto parse_args +) +if "%~1"=="--orders" ( + if "%~2"=="" ( + echo ERROR: --orders requires a number argument + exit /b 1 + ) + set "ORDERS=%~2" + shift + shift + goto parse_args +) +if "%~1"=="--reset" ( + set "RESET_FLAG=--reset" + shift + goto parse_args +) +shift +goto parse_args + +:start_generation +echo Configuration: +echo Products to generate: %PRODUCTS% +echo Orders to generate: %ORDERS% +if defined RESET_FLAG ( + echo Reset database: YES (WARNING: All data will be deleted!) +) else ( + echo Reset database: NO +) +echo. + +echo Checking Node.js availability... +node --version >nul 2>&1 +if errorlevel 1 ( + echo ERROR: Node.js is not installed or not in PATH + exit /b 1 +) + +echo Running test data generator... +node scripts/generate-test-data.js --products %PRODUCTS% --orders %ORDERS% %RESET_FLAG% + +if errorlevel 1 ( + echo. + echo ERROR: Test data generation failed! + exit /b 1 +) + +echo. +echo ======================================== +echo ✓ Test data generated successfully! +echo ======================================== +exit /b 0 diff --git a/Frontend/1 het/backend/scripts/generate-test-data.js b/Frontend/1 het/backend/scripts/generate-test-data.js new file mode 100644 index 0000000..30ef163 --- /dev/null +++ b/Frontend/1 het/backend/scripts/generate-test-data.js @@ -0,0 +1,263 @@ +#!/usr/bin/env node + +/** + * Script to generate bulk test data for the webstore database + * Usage: node scripts/generate-test-data.js [options] + * + * Options: + * --products N Generate N additional products (default: 10) + * --orders N Generate N test orders (default: 5) + * --reset Delete all data before seeding (careful!) + */ + +require("dotenv").config({ path: ".env" }); + +const { PrismaClient } = require("@prisma/client"); +const bcryptjs = require("bcryptjs"); + +const prisma = new PrismaClient(); + +// Parse command line arguments +const args = process.argv.slice(2); +const options = { + productsCount: 10, + ordersCount: 5, + shouldReset: false +}; + +for (let i = 0; i < args.length; i++) { + if (args[i] === "--products" && args[i + 1]) { + options.productsCount = parseInt(args[i + 1], 10); + i++; + } else if (args[i] === "--orders" && args[i + 1]) { + options.ordersCount = parseInt(args[i + 1], 10); + i++; + } else if (args[i] === "--reset") { + options.shouldReset = true; + } +} + +const productNames = [ + "Premium Leather Boots", + "Summer Canvas Shoes", + "Minimalist Sneakers", + "Casual Loafers", + "Running Shoes Pro", + "Hiking Boots", + "Beach Sandals", + "Formal Dress Shoes", + "Winter Snow Boots", + "Slip-on Comfort Shoes", + "Designer Heels", + "Waterproof Outdoor Boots", + "Lightweight Mesh Runners", + "Classic Oxfords", + "Athletic Training Shoes" +]; + +const descriptions = [ + "Premium quality with exceptional comfort", + "Perfect for everyday wear and activities", + "Stylish design with modern aesthetics", + "Durable materials built to last", + "Eco-friendly sustainable production", + "Handcrafted with attention to detail", + "Weather-resistant and waterproof", + "Ergonomic design for all-day comfort", + "Available in multiple colors", + "Perfect for professional environments" +]; + +function generateRandomPrice() { + return (Math.floor(Math.random() * 20) + 5).toString() + "990.00"; +} + +function generateRandomStock() { + return Math.floor(Math.random() * 50) + 5; +} + +function getRandomElement(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +async function resetDatabase() { + console.log("⚠️ Resetting database..."); + try { + // Delete in order of dependencies + await prisma.orderItem.deleteMany({}); + await prisma.order.deleteMany({}); + await prisma.product.deleteMany({}); + await prisma.category.deleteMany({}); + await prisma.user.deleteMany({}); + console.log("✓ Database reset completed"); + } catch (error) { + console.error("Error resetting database:", error.message); + throw error; + } +} + +async function seedInitialData() { + console.log("Seeding initial categories..."); + + const categories = [ + { name: "Shoes", slug: "shoes" }, + { name: "Bags", slug: "bags" }, + { name: "Accessories", slug: "accessories" }, + { name: "Clothing", slug: "clothing" }, + { name: "Electronics", slug: "electronics" } + ]; + + for (const category of categories) { + await prisma.category.upsert({ + where: { slug: category.slug }, + update: {}, + create: category + }); + } + + console.log("✓ Categories seeded"); +} + +async function generateProducts(count) { + console.log(`\nGenerating ${count} additional products...`); + + const categories = await prisma.category.findMany(); + const generatedProducts = []; + + for (let i = 0; i < count; i++) { + const category = getRandomElement(categories); + const name = `${getRandomElement(productNames)} ${i + 1}`; + + try { + const product = await prisma.product.create({ + data: { + name, + description: getRandomElement(descriptions), + price: generateRandomPrice(), + stock: generateRandomStock(), + categoryId: category.id, + imageUrl: `/images/placeholder-${i + 1}.jpg` + } + }); + generatedProducts.push(product); + + if ((i + 1) % 5 === 0) { + process.stdout.write(`\r Progress: ${i + 1}/${count} products`); + } + } catch (error) { + if (!error.message.includes("Unique constraint failed")) { + console.error(`\nError creating product: ${error.message}`); + } + } + } + + console.log(`\r✓ Generated ${generatedProducts.length} products`); + return generatedProducts; +} + +async function generateTestUsers(count = 5) { + console.log(`\nGenerating ${count} test users...`); + + const users = []; + for (let i = 1; i <= count; i++) { + const email = `user${i}@test.com`; + const passwordHash = await bcryptjs.hash("password123", 10); + + try { + const user = await prisma.user.upsert({ + where: { email }, + update: {}, + create: { + name: `Test User ${i}`, + email, + passwordHash + } + }); + users.push(user); + } catch (error) { + console.error(`Error creating user: ${error.message}`); + } + } + + console.log(`✓ Generated ${users.length} test users`); + return users; +} + +async function generateOrders(count) { + console.log(`\nGenerating ${count} test orders...`); + + const products = await prisma.product.findMany({ take: 20 }); + const createdOrders = []; + + for (let i = 0; i < count; i++) { + try { + const itemsCount = Math.floor(Math.random() * 3) + 1; + const items = []; + let totalPrice = 0; + + for (let j = 0; j < itemsCount; j++) { + const product = getRandomElement(products); + const quantity = Math.floor(Math.random() * 3) + 1; + items.push({ + productId: product.id, + quantity, + unitPrice: product.price + }); + totalPrice += parseFloat(product.price) * quantity; + } + + const order = await prisma.order.create({ + data: { + customerName: `Customer ${i + 1}`, + customerEmail: `customer${i + 1}@example.com`, + totalPrice: totalPrice.toFixed(2), + items: { + create: items + } + }, + include: { + items: true + } + }); + + createdOrders.push(order); + + if ((i + 1) % 2 === 0) { + process.stdout.write(`\r Progress: ${i + 1}/${count} orders`); + } + } catch (error) { + console.error(`\nError creating order: ${error.message}`); + } + } + + console.log(`\r✓ Generated ${createdOrders.length} orders`); + return createdOrders; +} + +async function main() { + console.log("🚀 Test Data Generator for Webstore\n"); + console.log("Options:"); + console.log(` Products to generate: ${options.productsCount}`); + console.log(` Orders to generate: ${options.ordersCount}`); + console.log(` Reset database first: ${options.shouldReset ? "YES" : "NO"}\n`); + + try { + if (options.shouldReset) { + await resetDatabase(); + } + + await seedInitialData(); + await generateTestUsers(5); + await generateProducts(options.productsCount); + await generateOrders(options.ordersCount); + + console.log("\n✨ Test data generation completed successfully!"); + } catch (error) { + console.error("\n❌ Error during data generation:", error.message); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +main(); diff --git a/Frontend/1 het/backend/src/Api/Controller/shop.controller.js b/Frontend/1 het/backend/src/Api/Controller/shop.controller.js index a4a72b1..31a6a37 100644 --- a/Frontend/1 het/backend/src/Api/Controller/shop.controller.js +++ b/Frontend/1 het/backend/src/Api/Controller/shop.controller.js @@ -1,13 +1,18 @@ const GetCategoriesQuery = require("../../Application/Shop/Query/GetCategoriesQuery"); const GetProductsQuery = require("../../Application/Shop/Query/GetProductsQuery"); +const GetProductByIdQuery = require("../../Application/Shop/Query/GetProductByIdQuery"); const CreateOrderCommand = require("../../Application/Shop/Command/CreateOrderCommand"); const CreateProductCommand = require("../../Application/Shop/Command/CreateProductCommand"); +const SetProductImageCommand = require("../../Application/Shop/Command/SetProductImageCommand"); +const ProductDTO = require("../../Application/DTO/ProductDTO"); const container = require("../../Infrastructure/DI/container"); const getCategoriesHandler = container.resolve("GetCategoriesQueryHandler"); const getProductsHandler = container.resolve("GetProductsQueryHandler"); +const getProductByIdHandler = container.resolve("GetProductByIdQueryHandler"); const createOrderHandler = container.resolve("CreateOrderCommandHandler"); const createProductHandler = container.resolve("CreateProductCommandHandler"); +const setProductImageHandler = container.resolve("SetProductImageCommandHandler"); const getCategories = async (req, res, next) => { try { @@ -22,7 +27,19 @@ const getCategories = async (req, res, next) => { const getProducts = async (req, res, next) => { try { const query = new GetProductsQuery(); - const data = await getProductsHandler.handle(query); + const products = await getProductsHandler.handle(query); + const data = products.map((product) => new ProductDTO(product, req)); + return res.json({ data }); + } catch (error) { + return next(error); + } +}; + +const getProductById = async (req, res, next) => { + try { + const query = new GetProductByIdQuery(req.params.productId); + const product = await getProductByIdHandler.handle(query); + const data = new ProductDTO(product, req); return res.json({ data }); } catch (error) { return next(error); @@ -31,8 +48,13 @@ const getProducts = async (req, res, next) => { const createProduct = async (req, res, next) => { try { - const command = new CreateProductCommand(req.body); - const data = await createProductHandler.handle(command); + const payload = { + ...req.body, + image_url: req.file ? `/images/uploads/${req.file.filename}` : req.body.image_url + }; + const command = new CreateProductCommand(payload); + const product = await createProductHandler.handle(command); + const data = new ProductDTO(product, req); return res.status(201).json({ data }); } catch (error) { return next(error); @@ -49,9 +71,29 @@ const createOrder = async (req, res, next) => { } }; +const setProductImage = async (req, res, next) => { + try { + if (!req.file) { + const error = new Error("Image file is required"); + error.statusCode = 400; + throw error; + } + + const imageUrl = `/images/uploads/${req.file.filename}`; + const command = new SetProductImageCommand(req.params.productId, imageUrl); + const product = await setProductImageHandler.handle(command); + const data = new ProductDTO(product, req); + return res.json({ data }); + } catch (error) { + return next(error); + } +}; + module.exports = { getCategories, getProducts, + getProductById, createProduct, - createOrder + createOrder, + setProductImage }; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Api/Middleware/errorHandler.js b/Frontend/1 het/backend/src/Api/Middleware/errorHandler.js index 8fed651..5b8fe73 100644 --- a/Frontend/1 het/backend/src/Api/Middleware/errorHandler.js +++ b/Frontend/1 het/backend/src/Api/Middleware/errorHandler.js @@ -3,6 +3,11 @@ const errorHandler = (err, req, res, next) => { return next(err); } + if (err.name === "MulterError") { + const message = err.code === "LIMIT_FILE_SIZE" ? "Image size must be <= 5MB" : err.message; + return res.status(400).json({ message }); + } + const status = err.statusCode || 500; return res.status(status).json({ message: err.message || "Internal server error" diff --git a/Frontend/1 het/backend/src/Api/Middleware/upload.middleware.js b/Frontend/1 het/backend/src/Api/Middleware/upload.middleware.js new file mode 100644 index 0000000..99c707d --- /dev/null +++ b/Frontend/1 het/backend/src/Api/Middleware/upload.middleware.js @@ -0,0 +1,42 @@ +const fs = require("fs"); +const path = require("path"); +const multer = require("multer"); + +const uploadDir = path.join(__dirname, "..", "..", "..", "images", "uploads"); +fs.mkdirSync(uploadDir, { recursive: true }); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + const safeBaseName = path + .parse(file.originalname) + .name + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 40) || "image"; + const ext = path.extname(file.originalname).toLowerCase(); + cb(null, `${Date.now()}-${safeBaseName}${ext}`); + } +}); + +const fileFilter = (req, file, cb) => { + if (!file.mimetype || !file.mimetype.startsWith("image/")) { + const error = new Error("Only image files are allowed"); + error.statusCode = 400; + return cb(error); + } + cb(null, true); +}; + +const upload = multer({ + storage, + fileFilter, + limits: { + fileSize: 5 * 1024 * 1024 + } +}); + +module.exports = upload; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Api/Router/shop.router.js b/Frontend/1 het/backend/src/Api/Router/shop.router.js index 04a0ab4..5c49430 100644 --- a/Frontend/1 het/backend/src/Api/Router/shop.router.js +++ b/Frontend/1 het/backend/src/Api/Router/shop.router.js @@ -1,11 +1,14 @@ const express = require("express"); const controller = require("../Controller/shop.controller"); +const upload = require("../Middleware/upload.middleware"); const router = express.Router(); router.get("/categories", controller.getCategories); router.get("/products", controller.getProducts); -router.post("/products", controller.createProduct); +router.get("/products/:productId", controller.getProductById); +router.post("/products", upload.single("image"), controller.createProduct); +router.post("/products/:productId/image", upload.single("image"), controller.setProductImage); router.post("/orders", controller.createOrder); module.exports = router; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Api/app.js b/Frontend/1 het/backend/src/Api/app.js index 78e58f5..6b611eb 100644 --- a/Frontend/1 het/backend/src/Api/app.js +++ b/Frontend/1 het/backend/src/Api/app.js @@ -1,4 +1,5 @@ const express = require("express"); +const path = require("path"); const helmet = require("helmet"); const cors = require("cors"); const morgan = require("morgan"); @@ -16,6 +17,7 @@ app.use(cors({ origin: true, credentials: true })); app.use(morgan("dev")); app.use(express.json()); app.use(cookieParser()); +app.use("/images", express.static(path.resolve(__dirname, "../../images"))); app.get("/api/health", (req, res) => { res.json({ status: "ok", message: "CQRS backend running" }); diff --git a/Frontend/1 het/backend/src/Application/DTO/ProductDTO.js b/Frontend/1 het/backend/src/Application/DTO/ProductDTO.js new file mode 100644 index 0000000..6bde60c --- /dev/null +++ b/Frontend/1 het/backend/src/Application/DTO/ProductDTO.js @@ -0,0 +1,28 @@ +class ProductDTO { + constructor(product, req) { + this.id = product.id; + this.category_id = product.categoryId; + this.category_name = product.category ? product.category.name : null; + this.name = product.name; + this.description = product.description; + this.price = Number(product.price); + this.image_url = ProductDTO.#buildImageUrl(product.imageUrl, req); + this.stock = product.stock; + this.created_at = product.createdAt; + } + + static #buildImageUrl(imageUrl, req) { + if (!imageUrl) { + return null; + } + + if (/^https?:\/\//i.test(imageUrl)) { + return imageUrl; + } + + const host = req.get("host"); + return `${req.protocol}://${host}${imageUrl}`; + } +} + +module.exports = ProductDTO; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Application/Shop/Command/SetProductImageCommand.js b/Frontend/1 het/backend/src/Application/Shop/Command/SetProductImageCommand.js new file mode 100644 index 0000000..2bdc8d7 --- /dev/null +++ b/Frontend/1 het/backend/src/Application/Shop/Command/SetProductImageCommand.js @@ -0,0 +1,8 @@ +class SetProductImageCommand { + constructor(productId, imageUrl) { + this.product_id = productId; + this.image_url = imageUrl; + } +} + +module.exports = SetProductImageCommand; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Application/Shop/Command/SetProductImageCommandHandler.js b/Frontend/1 het/backend/src/Application/Shop/Command/SetProductImageCommandHandler.js new file mode 100644 index 0000000..8bc9d18 --- /dev/null +++ b/Frontend/1 het/backend/src/Application/Shop/Command/SetProductImageCommandHandler.js @@ -0,0 +1,11 @@ +class SetProductImageCommandHandler { + constructor(shopRepository) { + this.shopRepository = shopRepository; + } + + async handle(command) { + return this.shopRepository.setProductImage(command.product_id, command.image_url); + } +} + +module.exports = SetProductImageCommandHandler; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Application/Shop/Query/GetProductByIdQuery.js b/Frontend/1 het/backend/src/Application/Shop/Query/GetProductByIdQuery.js new file mode 100644 index 0000000..295fe08 --- /dev/null +++ b/Frontend/1 het/backend/src/Application/Shop/Query/GetProductByIdQuery.js @@ -0,0 +1,7 @@ +class GetProductByIdQuery { + constructor(productId) { + this.product_id = productId; + } +} + +module.exports = GetProductByIdQuery; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Application/Shop/Query/GetProductByIdQueryHandler.js b/Frontend/1 het/backend/src/Application/Shop/Query/GetProductByIdQueryHandler.js new file mode 100644 index 0000000..9538568 --- /dev/null +++ b/Frontend/1 het/backend/src/Application/Shop/Query/GetProductByIdQueryHandler.js @@ -0,0 +1,11 @@ +class GetProductByIdQueryHandler { + constructor(shopRepository) { + this.shopRepository = shopRepository; + } + + async handle(query) { + return this.shopRepository.findProductById(query.product_id); + } +} + +module.exports = GetProductByIdQueryHandler; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Infrastructure/DI/container.js b/Frontend/1 het/backend/src/Infrastructure/DI/container.js index 899c49b..cd3443e 100644 --- a/Frontend/1 het/backend/src/Infrastructure/DI/container.js +++ b/Frontend/1 het/backend/src/Infrastructure/DI/container.js @@ -12,8 +12,10 @@ const GetCurrentUserQueryHandler = require("../../Application/User/Query/GetCurr const GetCategoriesQueryHandler = require("../../Application/Shop/Query/GetCategoriesQueryHandler"); const GetProductsQueryHandler = require("../../Application/Shop/Query/GetProductsQueryHandler"); +const GetProductByIdQueryHandler = require("../../Application/Shop/Query/GetProductByIdQueryHandler"); const CreateProductCommandHandler = require("../../Application/Shop/Command/CreateProductCommandHandler"); const CreateOrderCommandHandler = require("../../Application/Shop/Command/CreateOrderCommandHandler"); +const SetProductImageCommandHandler = require("../../Application/Shop/Command/SetProductImageCommandHandler"); const container = new DIContainer(); @@ -46,6 +48,10 @@ container.registerSingleton("GetProductsQueryHandler", (c) => { return new GetProductsQueryHandler(c.resolve("ShopRepository")); }); +container.registerSingleton("GetProductByIdQueryHandler", (c) => { + return new GetProductByIdQueryHandler(c.resolve("ShopRepository")); +}); + container.registerSingleton("CreateProductCommandHandler", (c) => { return new CreateProductCommandHandler(c.resolve("ShopRepository")); }); @@ -54,4 +60,8 @@ container.registerSingleton("CreateOrderCommandHandler", (c) => { return new CreateOrderCommandHandler(c.resolve("ShopRepository")); }); +container.registerSingleton("SetProductImageCommandHandler", (c) => { + return new SetProductImageCommandHandler(c.resolve("ShopRepository")); +}); + module.exports = container; \ No newline at end of file diff --git a/Frontend/1 het/backend/src/Infrastructure/Repository/ShopPrismaRepository.js b/Frontend/1 het/backend/src/Infrastructure/Repository/ShopPrismaRepository.js index 7305b70..14db682 100644 --- a/Frontend/1 het/backend/src/Infrastructure/Repository/ShopPrismaRepository.js +++ b/Frontend/1 het/backend/src/Infrastructure/Repository/ShopPrismaRepository.js @@ -14,6 +14,30 @@ class ShopPrismaRepository { }); } + async findProductById(productId) { + const id = Number(productId); + if (Number.isNaN(id)) { + const error = new Error("product_id must be a number"); + error.statusCode = 400; + throw error; + } + + const product = await prisma.product.findUnique({ + where: { id }, + include: { + category: true + } + }); + + if (!product) { + const error = new Error("Product not found"); + error.statusCode = 404; + throw error; + } + + return product; + } + async createProduct(payload) { const categoryId = Number(payload.category_id); const stock = payload.stock == null ? 0 : Number(payload.stock); @@ -121,6 +145,33 @@ class ShopPrismaRepository { return order; }); } + + async setProductImage(productId, imageUrl) { + const id = Number(productId); + + if (Number.isNaN(id) || !imageUrl) { + const error = new Error("product_id and image are required"); + error.statusCode = 400; + throw error; + } + + const product = await prisma.product.findUnique({ where: { id } }); + if (!product) { + const error = new Error("Product not found"); + error.statusCode = 404; + throw error; + } + + return prisma.product.update({ + where: { id }, + data: { + imageUrl + }, + include: { + category: true + } + }); + } } module.exports = ShopPrismaRepository; \ No newline at end of file