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 @@
+
+
+
+
+
+ Login
+
+
+
+
+
+
+
diff --git a/full-stack/ngnix/login.js b/full-stack/ngnix/login.js
new file mode 100644
index 0000000..f8c989f
--- /dev/null
+++ b/full-stack/ngnix/login.js
@@ -0,0 +1,38 @@
+document.addEventListener('DOMContentLoaded', function() {
+ const loginForm = document.querySelector('.form');
+
+ if (loginForm) {
+ loginForm.addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ const login = document.getElementById('login').value;
+ const password = document.getElementById('password').value;
+ const remember = document.querySelector('input[name="remember"]').checked;
+
+ try {
+ const response = await fetch('/api/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ name: login,
+ password: password
+ })
+ });
+
+ if (response.ok) {
+ // Login successful, redirect to workbench
+ window.location.href = '/workbench.html';
+ } else {
+ // Handle login error
+ const errorData = await response.json();
+ alert('Login failed: ' + (errorData.detail || 'Unknown error'));
+ }
+ } catch (error) {
+ console.error('Login error:', error);
+ alert('An error occurred during login. Please try again.');
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/full-stack/ngnix/logout.js b/full-stack/ngnix/logout.js
new file mode 100644
index 0000000..44b3eea
--- /dev/null
+++ b/full-stack/ngnix/logout.js
@@ -0,0 +1,34 @@
+document.addEventListener('DOMContentLoaded', function() {
+ // Add action buttons
+ const sidebarActions = document.querySelector('.sidebar-actions');
+ if (sidebarActions) {
+ // Add some example action buttons
+ for (let i = 1; i <= 5; i++) {
+ const actionButton = document.createElement('button');
+ actionButton.textContent = `Action ${i}`;
+ actionButton.className = 'action-button';
+ sidebarActions.appendChild(actionButton);
+ }
+
+ // Add logout button
+ const logoutButton = document.createElement('button');
+ logoutButton.textContent = 'Logout';
+ logoutButton.className = 'action-button';
+ logoutButton.style.marginTop = 'auto';
+ logoutButton.style.marginBottom = '20px';
+ sidebarActions.appendChild(logoutButton);
+
+ logoutButton.addEventListener('click', async function() {
+ try {
+ // Clear the session token cookie by setting it to expire
+ document.cookie = "session_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+
+ // Redirect to login page
+ window.location.href = '/login.html';
+ } catch (error) {
+ console.error('Logout error:', error);
+ alert('An error occurred during logout. Please try again.');
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/full-stack/ngnix/nginx.conf b/full-stack/ngnix/nginx.conf
new file mode 100644
index 0000000..e9fe2a7
--- /dev/null
+++ b/full-stack/ngnix/nginx.conf
@@ -0,0 +1,84 @@
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ server {
+ listen 80;
+ server_name localhost;
+ client_max_body_size 20M;
+
+ location / {
+ root /usr/share/nginx/html;
+ index login.html register.html register.js login.js logout.js workbench.js;
+ try_files $uri $uri/ =404;
+ }
+
+ location = /workbench.html {
+ auth_request /api/auth;
+ auth_request_set $auth_status $upstream_status;
+ root /usr/share/nginx/html;
+ error_page 401 = @error_401;
+ }
+
+ location @error_401 {
+ return 302 /login.html;
+ }
+
+ location /api/auth {
+ proxy_pass http://auth:8001;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+
+ location /api/register {
+ proxy_pass http://auth:8001;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /api/login {
+ proxy_pass http://auth:8001;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /api/photos {
+ auth_request /api/auth;
+ proxy_pass http://photo-upload:8002;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /api/photo_upload {
+ auth_request /api/auth;
+ proxy_pass http://photo-upload:8002;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ client_max_body_size 50M;
+ }
+
+ # Enable gzip compression
+ gzip on;
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ }
+}
\ No newline at end of file
diff --git a/full-stack/ngnix/register.html b/full-stack/ngnix/register.html
new file mode 100644
index 0000000..4f45398
--- /dev/null
+++ b/full-stack/ngnix/register.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ Register
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/full-stack/ngnix/register.js b/full-stack/ngnix/register.js
new file mode 100644
index 0000000..27f93a7
--- /dev/null
+++ b/full-stack/ngnix/register.js
@@ -0,0 +1,46 @@
+document.addEventListener('DOMContentLoaded', function() {
+ const registerForm = document.querySelector('.form');
+
+ if (registerForm) {
+ registerForm.addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ const name = document.getElementById('name').value;
+ const email = document.getElementById('email').value;
+ const password = document.getElementById('password').value;
+ const confirmPassword = document.getElementById('confirm-password').value;
+
+ // Check if passwords match
+ if (password !== confirmPassword) {
+ alert('Passwords do not match');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/register', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ name: name,
+ email: email,
+ password: password
+ })
+ });
+
+ if (response.ok) {
+ // Registration successful, redirect to workbench
+ window.location.href = '/workbench.html';
+ } else {
+ // Handle registration error
+ const errorData = await response.json();
+ alert('Registration failed: ' + (errorData.detail || 'Unknown error'));
+ }
+ } catch (error) {
+ console.error('Registration error:', error);
+ alert('An error occurred during registration. Please try again.');
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/full-stack/ngnix/styles.css b/full-stack/ngnix/styles.css
new file mode 100644
index 0000000..92564e5
--- /dev/null
+++ b/full-stack/ngnix/styles.css
@@ -0,0 +1,473 @@
+:root {
+ --background: #0f172a;
+ --panel: #111827;
+ --panel-border: #1f2937;
+ --text: #e5e7eb;
+ --muted: #94a3b8;
+ --primary: #4f46e5;
+ --primary-600: #4338ca;
+ --ring: #6366f1;
+ --danger: #ef4444;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
+ background: radial-gradient(1200px 800px at 10% 10%, #0b1024 0%, var(--background) 60%),
+ linear-gradient(135deg, #0b1024 0%, #0f172a 60%);
+ color: var(--text);
+ display: grid;
+ place-items: center;
+}
+
+.container {
+ width: 100%;
+ max-width: 420px;
+ padding: 24px;
+}
+
+.card {
+ background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
+ border: 1px solid var(--panel-border);
+ backdrop-filter: blur(8px);
+ border-radius: 16px;
+ padding: 28px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.35);
+}
+
+.header {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 22px;
+ text-align: center;
+}
+
+.header h1 {
+ margin: 0;
+ font-size: 28px;
+ letter-spacing: 0.2px;
+}
+
+.header p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 14px;
+}
+
+.form {
+ display: grid;
+ gap: 14px;
+}
+
+.label {
+ font-size: 13px;
+ color: var(--muted);
+}
+
+.input {
+ width: 100%;
+ padding: 12px 14px;
+ border: 1px solid var(--panel-border);
+ border-radius: 10px;
+ background: #0b1222;
+ color: var(--text);
+ outline: none;
+ transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
+}
+
+.input::placeholder { color: #7c8699; }
+
+.input:focus {
+ border-color: var(--ring);
+ box-shadow: 0 0 0 3px rgba(99,102,241,0.25);
+ background: #0c1427;
+}
+
+.row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.checkbox {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 13px;
+ color: var(--muted);
+}
+
+.checkbox input {
+ width: 16px;
+ height: 16px;
+ accent-color: var(--primary);
+}
+
+.link {
+ color: var(--ring);
+ text-decoration: none;
+ font-size: 13px;
+}
+
+.link:hover { text-decoration: underline; }
+
+.button {
+ margin-top: 6px;
+ width: 100%;
+ padding: 12px 16px;
+ border: 0;
+ border-radius: 10px;
+ background: linear-gradient(180deg, var(--primary), var(--primary-600));
+ color: white;
+ font-weight: 600;
+ letter-spacing: 0.2px;
+ cursor: pointer;
+ box-shadow: 0 8px 20px rgba(79,70,229,0.35);
+ transition: transform 120ms ease, filter 120ms ease, box-shadow 120ms ease;
+}
+
+.button:hover {
+ filter: brightness(1.05);
+ box-shadow: 0 10px 24px rgba(79,70,229,0.42);
+}
+
+.button:active { transform: translateY(1px); }
+
+.divider {
+ margin: 18px 0 8px;
+ text-align: center;
+ color: var(--muted);
+ font-size: 12px;
+ position: relative;
+}
+
+.divider::before,
+.divider::after {
+ content: "";
+ height: 1px;
+ background: var(--panel-border);
+ position: absolute;
+ top: 50%;
+ width: 40%;
+}
+
+.divider::before { left: 0; }
+.divider::after { right: 0; }
+
+.oauth {
+ display: grid;
+ gap: 10px;
+ grid-template-columns: 1fr 1fr;
+}
+
+.oauth button {
+ padding: 10px 12px;
+ border: 1px solid var(--panel-border);
+ background: #0b1222;
+ color: var(--text);
+ border-radius: 10px;
+ cursor: pointer;
+ transition: border-color 120ms ease, background 120ms ease;
+}
+
+.oauth button:hover {
+ border-color: var(--ring);
+ background: #0c1427;
+}
+
+.footer {
+ margin-top: 16px;
+ text-align: center;
+ font-size: 13px;
+ color: var(--muted);
+}
+
+.footer a { color: var(--ring); text-decoration: none; }
+.footer a:hover { text-decoration: underline; }
+
+@media (max-width: 420px) {
+ .card { padding: 22px; }
+}
+
+
+/* Workbench Styles */
+.container-workbench {
+ display: flex;
+ width: 100%;
+ max-width: 1200px;
+ height: 100vh;
+ padding: 20px;
+ gap: 20px;
+}
+
+.sidebar {
+ width: 250px;
+ background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
+ border: 1px solid var(--panel-border);
+ backdrop-filter: blur(8px);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.35);
+ display: flex;
+ flex-direction: column;
+}
+
+.sidebar-header h2 {
+ margin-top: 0;
+ margin-bottom: 20px;
+ font-size: 24px;
+ letter-spacing: 0.2px;
+}
+
+.sidebar-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.action-button {
+ padding: 12px 16px;
+ border: 1px solid var(--panel-border);
+ border-radius: 10px;
+ background: #0b1222;
+ color: var(--text);
+ font-weight: 500;
+ cursor: pointer;
+ transition: border-color 120ms ease, background 120ms ease;
+ text-align: left;
+}
+
+.action-button:hover {
+ border-color: var(--ring);
+ background: #0c1427;
+}
+
+.main-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.upload-section {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+}
+
+.upload-btn {
+ padding: 12px 20px;
+ border: 0;
+ border-radius: 10px;
+ background: linear-gradient(180deg, var(--primary), var(--primary-600));
+ color: white;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 4px 12px rgba(79,70,229,0.35);
+ transition: transform 120ms ease, filter 120ms ease, box-shadow 120ms ease;
+}
+
+.upload-btn:hover {
+ filter: brightness(1.05);
+ box-shadow: 0 6px 16px rgba(79,70,229,0.42);
+}
+
+.upload-btn:active {
+ transform: translateY(1px);
+}
+
+.upload-status {
+ padding: 8px 12px;
+ border-radius: 6px;
+ font-size: 14px;
+ margin-top: 5px;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.upload-status.show {
+ opacity: 1;
+}
+
+.upload-status.uploading {
+ background: #0b1222;
+ color: var(--muted);
+ opacity: 1;
+}
+
+.upload-status.success {
+ background: rgba(34, 197, 94, 0.15);
+ color: #22c55e;
+ opacity: 1;
+}
+
+.upload-status.error {
+ background: rgba(239, 68, 68, 0.15);
+ color: #ef4444;
+ opacity: 1;
+}
+
+.photo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 20px;
+ flex: 1;
+}
+
+.photo-card {
+ background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
+ border: 1px solid var(--panel-border);
+ border-radius: 12px;
+ padding: 20px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.25);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 200px;
+ cursor: pointer;
+ transition: transform 120ms ease, box-shadow 120ms ease;
+}
+
+.photo-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0,0,0,0.3);
+}
+
+.photo-thumbnail {
+ max-width: 100%;
+ max-height: 140px;
+ object-fit: cover;
+ border-radius: 8px;
+}
+
+.photo-name {
+ margin: 10px 0 0;
+ color: var(--muted);
+ font-size: 13px;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+}
+
+.upload-card {
+ cursor: pointer;
+ transition: transform 120ms ease, box-shadow 120ms ease;
+}
+
+.upload-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0,0,0,0.3);
+}
+
+.plus-icon {
+ font-size: 48px;
+ font-weight: 300;
+ color: var(--primary);
+ line-height: 1;
+}
+
+.photo-card p {
+ margin: 16px 0 0;
+ color: var(--muted);
+ font-size: 14px;
+}
+
+/* Modal Styles */
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0,0,0,0.85);
+}
+
+.modal-content {
+ margin: auto;
+ display: block;
+ width: 80%;
+ max-width: 800px;
+ max-height: 90vh;
+ object-fit: contain;
+ margin-top: 5vh;
+}
+
+.modal-content {
+ animation-name: zoom;
+ animation-duration: 0.3s;
+}
+
+@keyframes zoom {
+ from {transform:scale(0)}
+ to {transform:scale(1)}
+}
+
+.close {
+ position: absolute;
+ top: 20px;
+ right: 35px;
+ color: #f1f1f1;
+ font-size: 40px;
+ font-weight: bold;
+ transition: 0.3s;
+ z-index: 1001;
+}
+
+.close:hover,
+.close:focus {
+ color: #bbb;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+#modal-caption {
+ margin: auto;
+ display: block;
+ width: 80%;
+ max-width: 700px;
+ text-align: center;
+ color: #ccc;
+ padding: 10px 0;
+ height: 150px;
+}
+
+@media (max-width: 768px) {
+ .container-workbench {
+ flex-direction: column;
+ height: auto;
+ }
+
+ .sidebar {
+ width: 100%;
+ }
+
+ .photo-grid {
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ }
+
+ .modal-content {
+ width: 95%;
+ }
+
+ .close {
+ right: 15px;
+ top: 15px;
+ }
+}
\ No newline at end of file
diff --git a/full-stack/ngnix/workbench.html b/full-stack/ngnix/workbench.html
new file mode 100644
index 0000000..d1ae9c2
--- /dev/null
+++ b/full-stack/ngnix/workbench.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+ Workbench
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
![]()
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/full-stack/ngnix/workbench.js b/full-stack/ngnix/workbench.js
new file mode 100644
index 0000000..d473b1c
--- /dev/null
+++ b/full-stack/ngnix/workbench.js
@@ -0,0 +1,130 @@
+// Workbench JavaScript functionality
+
+document.addEventListener('DOMContentLoaded', function() {
+ // Get DOM elements
+ const uploadButton = document.getElementById('upload-button');
+ const photoUploadInput = document.getElementById('photo-upload');
+ const photoGrid = document.getElementById('photo-grid');
+ const uploadStatus = document.getElementById('upload-status');
+ const addPhotoCard = document.getElementById('add-photo-card');
+
+ // Modal elements
+ const modal = document.getElementById('photo-modal');
+ const modalImage = document.getElementById('modal-image');
+ const modalCaption = document.getElementById('modal-caption');
+ const closeModal = document.getElementsByClassName('close')[0];
+
+ // Event listener for upload button
+ uploadButton.addEventListener('click', function() {
+ photoUploadInput.click();
+ });
+
+ // Event listener for file selection
+ photoUploadInput.addEventListener('change', function(event) {
+ const file = event.target.files[0];
+ if (file) {
+ uploadPhoto(file);
+ }
+ });
+
+ // Event listener for add photo card
+ addPhotoCard.addEventListener('click', function() {
+ photoUploadInput.click();
+ });
+
+ // Event listener for modal close button
+ closeModal.addEventListener('click', function() {
+ modal.style.display = 'none';
+ });
+
+ // Event listener for clicking outside modal to close
+ window.addEventListener('click', function(event) {
+ if (event.target === modal) {
+ modal.style.display = 'none';
+ }
+ });
+
+ // Function to upload photo
+ function uploadPhoto(file) {
+ uploadStatus.textContent = 'Uploading...';
+ uploadStatus.className = 'upload-status uploading';
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ fetch('/api/photo_upload', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => {
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw new Error('Upload failed');
+ }
+ })
+ .then(data => {
+ uploadStatus.textContent = 'Upload successful!';
+ uploadStatus.className = 'upload-status success';
+ loadPhotos(); // Refresh photo grid
+ setTimeout(() => {
+ uploadStatus.textContent = '';
+ uploadStatus.className = 'upload-status';
+ }, 3000);
+ })
+ .catch(error => {
+ uploadStatus.textContent = 'Upload failed. Please try again.';
+ uploadStatus.className = 'upload-status error';
+ console.error('Upload error:', error);
+ });
+ }
+
+ // Function to load photos
+ function loadPhotos() {
+ fetch('/api/photos', {
+ method: 'GET',
+ credentials: 'include'
+ })
+ .then(response => response.json())
+ .then(data => {
+ displayPhotos(data.photos);
+ })
+ .catch(error => {
+ console.error('Error loading photos:', error);
+ });
+ }
+
+ // Function to display photos in grid
+ function displayPhotos(photos) {
+ // Clear existing photos except the add photo card
+ photoGrid.innerHTML = '';
+ photoGrid.appendChild(addPhotoCard);
+
+ // Add photos to grid
+ photos.forEach(photo => {
+ const photoCard = document.createElement('div');
+ photoCard.className = 'photo-card';
+ photoCard.innerHTML = `
+
+ ${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 = `${photo.created_at}
Uploaded: ${new Date(photo.created_at).toLocaleString()}
`;
+ modal.style.display = 'block';
+ }
+
+ // Load photos when page loads
+ loadPhotos();
+});
\ No newline at end of file