Photo Workbench
+Manage your photo collection
+Add Photo
+From 6756273be6ee2ede4672219f3821cd5034546f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=B3=E3=83=87=E3=83=AA=E3=83=A5?= <8221262@edu.miet.ru> Date: Sun, 28 Sep 2025 21:28:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20backend=20+=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- full-stack/AuthService/Dockerfile | 22 + full-stack/AuthService/auth.py | 167 +++++++ full-stack/AuthService/requirements.txt | 3 + full-stack/DB/Dockerfile | 21 + full-stack/FastAPI/Dockerfile | 13 + full-stack/FastAPI/main.py | 19 + full-stack/FastAPI/requirements.txt | 2 + full-stack/PhotoUploadService/Dockerfile | 22 + full-stack/PhotoUploadService/photo_upload.py | 181 +++++++ .../PhotoUploadService/requirements.txt | 5 + full-stack/docker-compose.yml | 68 +++ full-stack/ngnix/Dockerfile | 16 + full-stack/ngnix/README.md | 49 ++ full-stack/ngnix/login.html | 42 ++ full-stack/ngnix/login.js | 38 ++ full-stack/ngnix/logout.js | 34 ++ full-stack/ngnix/nginx.conf | 84 ++++ full-stack/ngnix/register.html | 40 ++ full-stack/ngnix/register.js | 46 ++ full-stack/ngnix/styles.css | 473 ++++++++++++++++++ full-stack/ngnix/workbench.html | 53 ++ full-stack/ngnix/workbench.js | 130 +++++ 22 files changed, 1528 insertions(+) create mode 100644 full-stack/AuthService/Dockerfile create mode 100644 full-stack/AuthService/auth.py create mode 100644 full-stack/AuthService/requirements.txt create mode 100644 full-stack/DB/Dockerfile create mode 100644 full-stack/FastAPI/Dockerfile create mode 100644 full-stack/FastAPI/main.py create mode 100644 full-stack/FastAPI/requirements.txt create mode 100644 full-stack/PhotoUploadService/Dockerfile create mode 100644 full-stack/PhotoUploadService/photo_upload.py create mode 100644 full-stack/PhotoUploadService/requirements.txt create mode 100644 full-stack/docker-compose.yml create mode 100644 full-stack/ngnix/Dockerfile create mode 100644 full-stack/ngnix/README.md create mode 100644 full-stack/ngnix/login.html create mode 100644 full-stack/ngnix/login.js create mode 100644 full-stack/ngnix/logout.js create mode 100644 full-stack/ngnix/nginx.conf create mode 100644 full-stack/ngnix/register.html create mode 100644 full-stack/ngnix/register.js create mode 100644 full-stack/ngnix/styles.css create mode 100644 full-stack/ngnix/workbench.html create mode 100644 full-stack/ngnix/workbench.js diff --git a/full-stack/AuthService/Dockerfile b/full-stack/AuthService/Dockerfile new file mode 100644 index 0000000..23119d8 --- /dev/null +++ b/full-stack/AuthService/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies for psycopg2 +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8001 + +# Run FastAPI with uvicorn +CMD ["uvicorn", "auth:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/full-stack/AuthService/auth.py b/full-stack/AuthService/auth.py new file mode 100644 index 0000000..2bf4017 --- /dev/null +++ b/full-stack/AuthService/auth.py @@ -0,0 +1,167 @@ +from fastapi import FastAPI, Cookie, HTTPException, Response +from pydantic import BaseModel +import psycopg2 +import os +import uuid +import hashlib + +app = FastAPI() + +DATABASE_URL = os.getenv("DATABASE_URL") + +class UserRegister(BaseModel): + name: str + email: str + password: str + +class UserLogin(BaseModel): + name: str + password: str + +def check_single_quote(*args): + """ + Check if any of the arguments contain single quotes + """ + for arg in args: + if isinstance(arg, str) and "'" in arg: + raise HTTPException(status_code=400, detail="Invalid input: contains single quote") + +@app.get("/") +def health(): + return {"Status": "ok"} + +@app.get("/api/auth") +def auth(session_token: str = Cookie(None)): + """ + make a request to DATABASE_URL and check if the user has session token + if user does not have session token, return 401 + if user has session token, return 200 + """ + if not session_token: + raise HTTPException(status_code=401, detail="No session token provided") + check_single_quote(session_token) + try: + conn = psycopg2.connect(DATABASE_URL) + cur = conn.cursor() + cur.execute("SELECT * FROM users WHERE session_token = %s", (session_token,)) + user = cur.fetchone() + cur.close() + conn.close() + + if user: + return Response(status_code=200) + else: + raise HTTPException(status_code=401, detail="Invalid session token") + except psycopg2.Error as e: + raise HTTPException(status_code=500, detail="Database error") + +@app.post("/api/auth") +def auth(session_token: str = Cookie(None)): + """ + make a request to DATABASE_URL and check if the user has session token + if user does not have session token, return 401 + if user has session token, return 200 + """ + if not session_token: + raise HTTPException(status_code=401, detail="No session token provided") + check_single_quote(session_token) + try: + conn = psycopg2.connect(DATABASE_URL) + cur = conn.cursor() + cur.execute("SELECT * FROM users WHERE session_token = %s", (session_token,)) + user = cur.fetchone() + cur.close() + conn.close() + + if user: + return Response(status_code=200) + else: + raise HTTPException(status_code=401, detail="Invalid session token") + except psycopg2.Error as e: + raise HTTPException(status_code=500, detail="Database error") + +@app.post("/api/register") +def register(user_data: UserRegister): + """ + make a request to DATABASE_URL and register the user + return the session token in cookie and status 200 + """ + try: + conn = psycopg2.connect(DATABASE_URL) + check_single_quote(user_data.email) + cur = conn.cursor() + + # Check if user already exists + cur.execute("SELECT * FROM users WHERE email = %s", (user_data.email,)) + existing_user = cur.fetchone() + + if existing_user: + cur.close() + conn.close() + raise HTTPException(status_code=400, detail="User already exists") + + # Hash the password + password_hash = hashlib.sha256(user_data.password.encode()).hexdigest() + + check_single_quote(user_data.name, user_data.email) + # Generate session token + session_token = str(uuid.uuid4()).replace('-', '')[:64] + + # Insert new user + cur.execute("INSERT INTO users (name, email, password_hash, session_token) VALUES (%s, %s, %s, %s)", + (user_data.name, user_data.email, password_hash, session_token)) + conn.commit() + + cur.close() + conn.close() + + # Create response with cookie + response = Response(status_code=200) + response.set_cookie(key="session_token", value=session_token, httponly=True) + return response + + except psycopg2.Error as e: + raise HTTPException(status_code=500, detail="Database error") + +@app.post("/api/login") +def login(user_data: UserLogin): + """ + make a request to DATABASE_URL and check user credentials + return the session token in cookie and status 200 + """ + try: + conn = psycopg2.connect(DATABASE_URL) + cur = conn.cursor() + check_single_quote(user_data.name) + + # Hash the provided password + password_hash = hashlib.sha256(user_data.password.encode()).hexdigest() + + # Check user credentials + cur.execute("SELECT * FROM users WHERE name = %s AND password_hash = %s", + (user_data.name, password_hash)) + user = cur.fetchone() + + if not user: + cur.close() + conn.close() + raise HTTPException(status_code=401, detail="Invalid credentials") + + # Generate new session token + session_token = str(uuid.uuid4()).replace('-', '')[:64] + + # Update user's session token + cur.execute("UPDATE users SET session_token = %s WHERE name = %s", + (session_token, user_data.name)) + conn.commit() + + cur.close() + conn.close() + + # Create response with cookie + response = Response(status_code=200) + response.set_cookie(key="session_token", value=session_token, httponly=True) + return response + + except psycopg2.Error as e: + raise HTTPException(status_code=500, detail="Database error") diff --git a/full-stack/AuthService/requirements.txt b/full-stack/AuthService/requirements.txt new file mode 100644 index 0000000..c25f9c9 --- /dev/null +++ b/full-stack/AuthService/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn[standard] +psycopg2-binary diff --git a/full-stack/DB/Dockerfile b/full-stack/DB/Dockerfile new file mode 100644 index 0000000..5a4955e --- /dev/null +++ b/full-stack/DB/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +# Use build arg to pick Postgres version; defaults to lightweight alpine variant +ARG POSTGRES_VERSION=16-alpine +FROM postgres:${POSTGRES_VERSION} + +# Configure default credentials and database (override at runtime via env vars) +ENV POSTGRES_USER=appuser \ + POSTGRES_PASSWORD=apppassword \ + POSTGRES_DB=appdb + +# Expose PostgreSQL default port +EXPOSE 5432 + +# Persist database files +VOLUME ["/var/lib/postgresql/data"] + +# Basic healthcheck to ensure DB is ready to accept connections +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" || exit 1 + + diff --git a/full-stack/FastAPI/Dockerfile b/full-stack/FastAPI/Dockerfile new file mode 100644 index 0000000..b70db4d --- /dev/null +++ b/full-stack/FastAPI/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Run FastAPI with uvicorn +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/full-stack/FastAPI/main.py b/full-stack/FastAPI/main.py new file mode 100644 index 0000000..d68ec75 --- /dev/null +++ b/full-stack/FastAPI/main.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def read_root(): + return {"message": "Hello from FastAPI!"} + +@app.get("/api") +def read_api_root(): + return {"message": "Welcome to the API"} + +@app.get("/api/items") +def read_items(): + return {"items": ["item1", "item2", "item3"]} + +@app.get("/api/items/{item_id}") +def read_item(item_id: int): + return {"item_id": item_id, "name": f"Item {item_id}"} diff --git a/full-stack/FastAPI/requirements.txt b/full-stack/FastAPI/requirements.txt new file mode 100644 index 0000000..364e2ee --- /dev/null +++ b/full-stack/FastAPI/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn[standard] diff --git a/full-stack/PhotoUploadService/Dockerfile b/full-stack/PhotoUploadService/Dockerfile new file mode 100644 index 0000000..962b67d --- /dev/null +++ b/full-stack/PhotoUploadService/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies for psycopg2 +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8002 + +# Run FastAPI with uvicorn +CMD ["uvicorn", "photo_upload:app", "--host", "0.0.0.0", "--port", "8002"] \ No newline at end of file diff --git a/full-stack/PhotoUploadService/photo_upload.py b/full-stack/PhotoUploadService/photo_upload.py new file mode 100644 index 0000000..9e59e91 --- /dev/null +++ b/full-stack/PhotoUploadService/photo_upload.py @@ -0,0 +1,181 @@ +from fastapi import FastAPI, File, UploadFile, Cookie, HTTPException, Response, Depends +from pydantic import BaseModel +import psycopg2 +import os +import uuid +import boto3 +from botocore.exceptions import ClientError +from typing import List, Optional +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = FastAPI() + +# Environment variables +DATABASE_URL = os.getenv("DATABASE_URL") +AWS_ACCESS_KEY_ID = os.getenv("S3_ACCESS_KEY") +AWS_SECRET_ACCESS_KEY = os.getenv("S3_SECRET_KEY") +AWS_ENDPOINT_URL = os.getenv("S3_ENDPOINT") +S3_BUCKET_NAME = os.getenv("S3_BUCKET") + +# Initialize S3 client +s3_client = boto3.client( + "s3", + aws_access_key_id=AWS_ACCESS_KEY_ID, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY, + endpoint_url=AWS_ENDPOINT_URL, +) + +class PhotoResponse(BaseModel): + id: int + owner_id: int + photo_url: str + created_at: str + +class UserPhotosResponse(BaseModel): + photos: List[PhotoResponse] + +def check_single_quote(*args): + """ + Check if any of the arguments contain single quotes + """ + for arg in args: + if isinstance(arg, str) and "'" in arg: + raise HTTPException(status_code=400, detail="Invalid input: contains single quote") + +def get_user_id_from_session(session_token: str) -> int: + """ + Get user ID from session token + """ + if not session_token: + raise HTTPException(status_code=401, detail="No session token provided") + + check_single_quote(session_token) + + try: + conn = psycopg2.connect(DATABASE_URL) + cur = conn.cursor() + cur.execute("SELECT id FROM users WHERE session_token = %s", (session_token,)) + user = cur.fetchone() + cur.close() + conn.close() + + if user: + return user[0] # Return user ID + else: + raise HTTPException(status_code=401, detail="Invalid session token") + except psycopg2.Error as e: + logger.error(f"Database error: {e}") + raise HTTPException(status_code=500, detail="Database error") + +@app.get("/") +def health(): + return {"Status": "ok"} + +@app.post("/api/photo_upload") +async def upload_photo( + file: UploadFile = File(...), + session_token: str = Cookie(None) +): + """ + Upload a photo file to S3 and save metadata to database + """ + # Authenticate user + owner_id = get_user_id_from_session(session_token) + + # Validate file + if not file: + raise HTTPException(status_code=400, detail="No file provided") + + # Generate unique filename + file_extension = file.filename.split('.')[-1] if '.' in file.filename else '' + unique_filename = f"{uuid.uuid4()}.{file_extension}" + + try: + # Upload file to S3 + s3_client.upload_fileobj( + file.file, + S3_BUCKET_NAME, + unique_filename, + ExtraArgs={'ContentType': file.content_type} + ) + + # Generate S3 URL + photo_url = unique_filename + + # Save metadata to database + conn = psycopg2.connect(DATABASE_URL) + cur = conn.cursor() + + cur.execute( + "INSERT INTO user_photos (owner_id, photo_url) VALUES (%s, %s) RETURNING id, created_at", + (owner_id, photo_url) + ) + + result = cur.fetchone() + photo_id = result[0] + created_at = result[1] + + conn.commit() + cur.close() + conn.close() + + return { + "id": photo_id, + "photo_url": photo_url, + "created_at": created_at.isoformat() if hasattr(created_at, 'isoformat') else str(created_at) + } + + except ClientError as e: + logger.error(f"S3 upload error: {e}") + raise HTTPException(status_code=500, detail="Failed to upload file to S3") + except psycopg2.Error as e: + logger.error(f"Database error: {e}") + raise HTTPException(status_code=500, detail="Database error") + except Exception as e: + logger.error(f"Unexpected error: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@app.get("/api/photos", response_model=UserPhotosResponse) +def get_user_photos(session_token: str = Cookie(None)): + """ + Fetch all photos for the authenticated user + """ + # Authenticate user + owner_id = get_user_id_from_session(session_token) + + try: + # Fetch photos from database + conn = psycopg2.connect(DATABASE_URL) + cur = conn.cursor() + + cur.execute( + "SELECT id, owner_id, photo_url, created_at FROM user_photos WHERE owner_id = %s ORDER BY created_at DESC", + (owner_id,) + ) + + rows = cur.fetchall() + cur.close() + conn.close() + + # Format response + photos = [] + for row in rows: + photos.append({ + "id": row[0], + "owner_id": row[1], + "photo_url": f"https://storage.yandexcloud.net/{S3_BUCKET_NAME}/{row[2]}", + "created_at": row[3].isoformat() if hasattr(row[3], 'isoformat') else str(row[3]) + }) + + return {"photos": photos} + + except psycopg2.Error as e: + logger.error(f"Database error: {e}") + raise HTTPException(status_code=500, detail="Database error") + except Exception as e: + logger.error(f"Unexpected error: {e}") + raise HTTPException(status_code=500, detail="Internal server error") \ No newline at end of file diff --git a/full-stack/PhotoUploadService/requirements.txt b/full-stack/PhotoUploadService/requirements.txt new file mode 100644 index 0000000..5444aa5 --- /dev/null +++ b/full-stack/PhotoUploadService/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +psycopg2-binary +boto3 +python-multipart \ No newline at end of file diff --git a/full-stack/docker-compose.yml b/full-stack/docker-compose.yml new file mode 100644 index 0000000..7f40da3 --- /dev/null +++ b/full-stack/docker-compose.yml @@ -0,0 +1,68 @@ +version: '3.8' + +services: + nginx: + build: ./ngnix + ports: + - "80:80" + depends_on: + - fastapi + - photo-upload + networks: + - app-network + + fastapi: + build: ./FastAPI + ports: + - "8000" + depends_on: + - db + environment: + - DATABASE_URL=postgresql://appuser:apppassword@db:5432/appdb + networks: + - app-network + + auth: + build: ./AuthService + ports: + - "8001" + depends_on: + - db + environment: + - DATABASE_URL=postgresql://appuser:apppassword@db:5432/appdb + networks: + - app-network + + photo-upload: + build: ./PhotoUploadService + ports: + - "8002" + depends_on: + - db + environment: + - DATABASE_URL=postgresql://appuser:apppassword@db:5432/appdb + - S3_BUCKET=web-server + - S3_ENDPOINT=https://storage.yandexcloud.net + - S3_ACCESS_KEY=YCAJE3It1Ok3S-dnBCUSAO6iW + - S3_SECRET_KEY=YCNY5wtY03s2JeCjY1uaX_2lATIkeyJ0pDKCxXCC + networks: + - app-network + + db: + build: ./DB + ports: + - "5432:5432" + environment: + - POSTGRES_USER=appuser + - POSTGRES_PASSWORD=apppassword + - POSTGRES_DB=appdb + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - app-network + +networks: + app-network: + driver: bridge +volumes: + pgdata: \ No newline at end of file diff --git a/full-stack/ngnix/Dockerfile b/full-stack/ngnix/Dockerfile new file mode 100644 index 0000000..4cc510b --- /dev/null +++ b/full-stack/ngnix/Dockerfile @@ -0,0 +1,16 @@ +# Use the official nginx image as the base image +FROM nginx:alpine + +# Copy the static files to the nginx html directory +COPY *.html /usr/share/nginx/html/ +COPY *.css /usr/share/nginx/html/ +COPY *.js /usr/share/nginx/html/ + +# Copy the nginx configuration file +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port 80 +EXPOSE 80 + +# Start nginx when the container runs +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/full-stack/ngnix/README.md b/full-stack/ngnix/README.md new file mode 100644 index 0000000..b430027 --- /dev/null +++ b/full-stack/ngnix/README.md @@ -0,0 +1,49 @@ +# Nginx Static File Server + +This project contains a Docker container configuration for serving static HTML and CSS files using nginx. + +## Files Included + +- `login.html` - Login page +- `register.html` - Registration page +- `styles.css` - Stylesheet for both pages + +## Docker Setup + +### Build the Docker Image + +```bash +docker build -t nginx-static-server . +``` + +### Run the Container + +```bash +docker run -d -p 8080:80 --name static-server nginx-static-server +``` + +### Access the Pages + +After running the container, you can access the pages at: +- Login page: http://localhost:8080/login.html +- Registration page: http://localhost:8080/register.html + +### Stop the Container + +```bash +docker stop static-server +``` + +### Remove the Container + +```bash +docker rm static-server +``` + +## Configuration + +The nginx server is configured to: +- Serve static files from the `/usr/share/nginx/html/` directory +- Listen on port 80 +- Include security headers +- Enable gzip compression for text-based files \ No newline at end of file diff --git a/full-stack/ngnix/login.html b/full-stack/ngnix/login.html new file mode 100644 index 0000000..8c055cd --- /dev/null +++ b/full-stack/ngnix/login.html @@ -0,0 +1,42 @@ + + +
+ + +Sign in to your account to continue
+Sign up for an account to get started
+Manage your photo collection
+Add Photo
+${photo.created_at}
+ `; + + // Add click event to open modal + photoCard.addEventListener('click', function() { + openPhotoModal(photo); + }); + + photoGrid.insertBefore(photoCard, addPhotoCard); + }); + } + + // Function to open photo in modal + function openPhotoModal(photo) { + modalImage.src = photo.photo_url; + modalCaption.innerHTML = `Uploaded: ${new Date(photo.created_at).toLocaleString()}
`; + modal.style.display = 'block'; + } + + // Load photos when page loads + loadPhotos(); +}); \ No newline at end of file