SK1
This commit is contained in:
parent
1002e93c93
commit
e581827e42
269
SK1/README.md
Normal file
269
SK1/README.md
Normal file
@ -0,0 +1,269 @@
|
||||
# 🔐 PasteVault
|
||||
|
||||
A secure, self-hosted code and text sharing tool with syntax highlighting, expiring links, and optional password protection. Think Pastebin — but private, expiring, and deployed on Azure.
|
||||
|
||||
---
|
||||
|
||||
## What the Application Does
|
||||
|
||||
PasteVault allows users to:
|
||||
- Paste code or text and receive a unique shareable URL
|
||||
- Select a programming language for syntax highlighting (15 languages supported)
|
||||
- Set an expiry time (1 hour, 1 day, 7 days, 30 days, or never)
|
||||
- Optionally password-protect a paste
|
||||
- Share the link — only people with the URL (and password if set) can view it
|
||||
- Copy paste content or the share URL with one click
|
||||
- View metadata: creation time, expiry countdown, view count
|
||||
|
||||
Pastes are automatically deleted from the database when they expire.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The application uses a **hybrid cloud architecture**:
|
||||
|
||||
- **Azure VM** (`Standard_D2s_v3` — 2 vCPU, 8 GB RAM, West Europe) runs all application containers via Docker Compose
|
||||
- **Azure PostgreSQL Flexible Server** (`Standard_B1MS`, North Europe) stores all paste data persistently
|
||||
- **Azure Blob Storage** (North Europe) stores database backup dumps
|
||||
- **Cloudflare** (external, pre-existing) provides DNS, HTTPS/TLS termination, and access logs
|
||||
|
||||
### Containers (3 total)
|
||||
|
||||
| Container | Image | Role |
|
||||
|---|---|---|
|
||||
| `pastevault-frontend` | Custom (Nginx + React) | Serves the UI and proxies `/api/` to backend |
|
||||
| `pastevault-backend` | Custom (Python FastAPI) | REST API — paste CRUD, expiry logic |
|
||||
| `pastevault-redis` | `redis:7-alpine` | Caching paste reads, reducing DB load |
|
||||
|
||||
### Communication Flow
|
||||
|
||||
```
|
||||
Browser → Cloudflare (HTTPS) → VM port 80 → Nginx (frontend container)
|
||||
↓ /api/* proxy
|
||||
FastAPI (backend container)
|
||||
↓
|
||||
Redis (cache, 30s TTL)
|
||||
↓ cache miss
|
||||
Azure PostgreSQL (persistent store)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cloud Services Used
|
||||
|
||||
| Service | Purpose | Tier |
|
||||
|---|---|---|
|
||||
| Azure VM `Standard_D2s_v3` | Runs Docker Compose with all 3 containers | Pay-as-you-go |
|
||||
| Azure PostgreSQL Flexible Server | Managed relational database | Burstable B1MS (free tier) |
|
||||
| Azure Blob Storage | Backup storage for `pg_dump` files | Standard LRS |
|
||||
| Cloudflare (pre-existing) | DNS, HTTPS certificate, access logs | Free plan |
|
||||
|
||||
### Persistent Storage
|
||||
- **PostgreSQL** stores all paste records (id, title, content, language, expiry, views, password hash)
|
||||
- **Redis Docker volume** (`redis_data`) persists cache data across restarts using `--appendonly yes`
|
||||
- **Azure Blob Storage** holds scheduled database backups
|
||||
|
||||
### Auto-restart
|
||||
All three containers use `restart: always` in `docker-compose.yml`. If any container crashes, Docker restarts it automatically without any manual intervention.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
sk1/
|
||||
├── backend/
|
||||
│ ├── main.py # FastAPI app — all API endpoints, DB logic, Redis caching
|
||||
│ ├── requirements.txt # Python dependencies
|
||||
│ └── Dockerfile # Python 3.12 slim image
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── main.jsx # React entry point
|
||||
│ │ ├── App.jsx # Router — two routes: / and /paste/:id
|
||||
│ │ ├── CreatePaste.jsx # Paste creation form
|
||||
│ │ ├── ViewPaste.jsx # Paste viewer with syntax highlighting
|
||||
│ │ └── index.css # Dark theme stylesheet
|
||||
│ ├── index.html # HTML shell
|
||||
│ ├── package.json # Node dependencies (React, Vite, highlight.js)
|
||||
│ ├── vite.config.js # Vite build config with API proxy for local dev
|
||||
│ ├── nginx.conf # Nginx config — serves React, proxies /api/ to backend
|
||||
│ └── Dockerfile # Multi-stage: Node build → Nginx serve
|
||||
├── scripts/
|
||||
│ ├── prepare-app.sh # Full Azure provisioning + deployment script
|
||||
│ ├── remove-app.sh # Teardown script — stops containers + deletes all Azure resources
|
||||
│ └── backup.sh # pg_dump → Azure Blob Storage
|
||||
├── docker-compose.yml # Defines frontend, backend, redis services
|
||||
├── .env.example # Template for environment variables (safe to commit)
|
||||
├── .gitignore # Excludes .env, node_modules, __pycache__, dist
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is done via environment variables in the `.env` file. This file is **never committed to Git**.
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `DATABASE_URL` | PostgreSQL connection string (without `?ssl=`) |
|
||||
| `REDIS_URL` | Redis connection string (default: `redis://redis:6379`) |
|
||||
| `SECRET_KEY` | Used to salt password hashes |
|
||||
| `DB_PASS` | PostgreSQL admin password (used by backup script) |
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your values before running any scripts.
|
||||
|
||||
---
|
||||
|
||||
## How to Run the Application
|
||||
|
||||
### Prerequisites
|
||||
- Azure CLI installed and logged in (`az login`)
|
||||
- SSH key at `~/.ssh/id_rsa`
|
||||
- `.env` file filled in
|
||||
|
||||
### Deploy
|
||||
```bash
|
||||
cd sk1/
|
||||
bash scripts/prepare-app.sh
|
||||
```
|
||||
|
||||
This script automatically:
|
||||
1. Creates the Azure resource group
|
||||
2. Provisions the VM (Standard_D2s_v3, West Europe)
|
||||
3. Creates PostgreSQL Flexible Server (B1MS, North Europe)
|
||||
4. Creates Azure Blob Storage for backups
|
||||
5. Installs Docker on the VM
|
||||
6. Copies app files to the VM
|
||||
7. Builds and starts all containers
|
||||
|
||||
### Teardown
|
||||
```bash
|
||||
bash scripts/remove-app.sh
|
||||
```
|
||||
|
||||
Stops all containers and deletes the entire Azure resource group (VM, database, storage, IP).
|
||||
|
||||
### Conditions to run scripts
|
||||
- `prepare-app.sh`: Azure CLI logged in, `.env` present, SSH key at `~/.ssh/id_rsa`, no existing `pastevault-rg` resource group
|
||||
- `remove-app.sh`: Azure CLI logged in, SSH access to VM still working
|
||||
|
||||
---
|
||||
|
||||
## Using the Application
|
||||
|
||||
1. Open the app URL in a browser (`https://yourdomain.com`)
|
||||
2. Enter a title, paste your code or text into the editor
|
||||
3. Select the language for syntax highlighting
|
||||
4. Choose an expiry time (or never)
|
||||
5. Optionally set a password
|
||||
6. Click **"🔒 Create Paste"**
|
||||
7. You are redirected to the paste URL — copy it with the **"🔗 Share"** button
|
||||
8. Anyone with the link can view the paste (and password if set)
|
||||
|
||||
---
|
||||
|
||||
## Backup
|
||||
|
||||
Run from the project root on your local machine (requires `postgresql-client`):
|
||||
|
||||
```bash
|
||||
bash scripts/backup.sh
|
||||
```
|
||||
|
||||
This dumps the entire `pastevault` database with `pg_dump` and uploads it to Azure Blob Storage.
|
||||
|
||||
**List all backups:**
|
||||
```bash
|
||||
az storage blob list \
|
||||
--account-name pastevaultstorage \
|
||||
--container-name backups \
|
||||
--output table
|
||||
```
|
||||
|
||||
**Download a specific backup:**
|
||||
```bash
|
||||
az storage blob download \
|
||||
--account-name pastevaultstorage \
|
||||
--container-name backups \
|
||||
--name pastevault_backup_YYYYMMDD_HHMMSS.sql \
|
||||
--file ./restore.sql
|
||||
```
|
||||
|
||||
**Restore from backup:**
|
||||
```bash
|
||||
PGPASSWORD=yourpassword psql \
|
||||
--host=pastevault-db.postgres.database.azure.com \
|
||||
--username=pvadmin \
|
||||
--dbname=pastevault \
|
||||
--file=./restore.sql \
|
||||
--sslmode=require
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Viewing Access Logs
|
||||
|
||||
### From Cloudflare (internet traffic)
|
||||
Cloudflare dashboard → your domain → **Analytics** tab shows:
|
||||
- Total requests, unique visitors
|
||||
- Geographic breakdown
|
||||
- Traffic over time
|
||||
- Blocked threats
|
||||
|
||||
### From Nginx (per-request logs)
|
||||
```bash
|
||||
ssh azureuser@YOUR_VM_IP
|
||||
docker logs pastevault-frontend
|
||||
```
|
||||
|
||||
Each line shows: IP address, timestamp, HTTP method, path, status code, response size.
|
||||
|
||||
### From the backend
|
||||
```bash
|
||||
docker logs pastevault-backend
|
||||
```
|
||||
|
||||
### Live log stream
|
||||
```bash
|
||||
docker logs -f pastevault-frontend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cost Analysis — 1000 Users/Day, 50 GB Data
|
||||
|
||||
| Resource | Spec | Billing | Monthly | Annual |
|
||||
|---|---|---|---|---|
|
||||
| Azure VM `Standard_D2s_v3` | 2 vCPU, 8 GB, West Europe | Per hour | ~$7.30 | ~$87.60 |
|
||||
| Azure PostgreSQL B1MS | 1 vCPU, 2 GB, 50 GB storage | Per hour + GB | ~$13.80 | ~$165.60 |
|
||||
| Azure Blob Storage | 50 GB backups, LRS | Per GB/month | ~$1.00 | ~$12.00 |
|
||||
| Azure Public IP | Standard static IP | Per hour | ~$3.00 | ~$36.00 |
|
||||
| Cloudflare | DNS + HTTPS + CDN | — | $0 | $0 |
|
||||
| **Total** | | | **~$25.10** | **~$301.20** |
|
||||
|
||||
At 1000 users/day the PostgreSQL B1MS handles load comfortably. For 10,000+ users/day, scaling to `Standard_D2s_v3` for the DB (~$75/month) would be recommended.
|
||||
|
||||
The biggest cost saving in this architecture is using Cloudflare as the HTTPS/CDN layer for free instead of an Azure Application Gateway (~$120/month).
|
||||
|
||||
---
|
||||
|
||||
## Known Issues Encountered During Deployment
|
||||
|
||||
- **Azure B-series VMs unavailable in West Europe and North Europe** — at time of deployment, `Standard_B1s`, `Standard_B2s`, and `Standard_B2ats_v2` had capacity restrictions across multiple European regions. Resolved by using `Standard_D2s_v3` in West Europe.
|
||||
- **PostgreSQL SSL configuration conflict** — `asyncpg` does not accept `?ssl=require` in the connection URL when also passed as a keyword argument. Resolved by keeping `ssl=True` in code only and removing it from the `DATABASE_URL`.
|
||||
- **Docker Compose `version` field warning** — Docker Compose v2 treats the `version` key as obsolete. Harmless warning, does not affect functionality.
|
||||
|
||||
---
|
||||
|
||||
## External Resources
|
||||
|
||||
| Resource | Type | Usage |
|
||||
|---|---|---|
|
||||
| FastAPI documentation | Official docs | API structure, startup events, dependency injection |
|
||||
| asyncpg documentation | Official docs | PostgreSQL async connection pool configuration |
|
||||
| highlight.js | Open source library | Client-side syntax highlighting for 15 languages |
|
||||
| Azure CLI documentation | Official docs | VM, PostgreSQL, and storage provisioning commands |
|
||||
| Docker documentation | Official docs | Multi-stage Dockerfile, Compose configuration |
|
||||
| **Claude (Anthropic)** | Generative AI | Used to generate application code, Dockerfiles, deployment scripts, and this documentation. All output reviewed and tested manually. |
|
||||
12
SK1/backend/Dockerfile
Normal file
12
SK1/backend/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
169
SK1/backend/main.py
Normal file
169
SK1/backend/main.py
Normal file
@ -0,0 +1,169 @@
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import asyncpg
|
||||
import redis.asyncio as aioredis
|
||||
import uuid
|
||||
import hashlib
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = FastAPI(title="PasteVault API")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379")
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "changeme")
|
||||
|
||||
db_pool = None
|
||||
redis_client = None
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
global db_pool, redis_client
|
||||
db_pool = await asyncpg.create_pool(DATABASE_URL, ssl="require")
|
||||
redis_client = await aioredis.from_url(REDIS_URL, decode_responses=True)
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS pastes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title VARCHAR(255) NOT NULL DEFAULT 'Untitled',
|
||||
content TEXT NOT NULL,
|
||||
language VARCHAR(50) DEFAULT 'plaintext',
|
||||
password_hash VARCHAR(255),
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
views INTEGER DEFAULT 0
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await db_pool.close()
|
||||
await redis_client.aclose()
|
||||
|
||||
|
||||
class PasteCreate(BaseModel):
|
||||
title: str = "Untitled"
|
||||
content: str
|
||||
language: str = "plaintext"
|
||||
password: Optional[str] = None
|
||||
expiry: Optional[str] = None
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return hashlib.sha256((password + SECRET_KEY).encode()).hexdigest()
|
||||
|
||||
|
||||
def get_expiry(expiry_str: Optional[str]) -> Optional[datetime]:
|
||||
mapping = {"1h": timedelta(hours=1), "1d": timedelta(days=1),
|
||||
"7d": timedelta(days=7), "30d": timedelta(days=30)}
|
||||
if not expiry_str or expiry_str not in mapping:
|
||||
return None
|
||||
return datetime.utcnow() + mapping[expiry_str]
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
return {"status": "ok", "service": "PasteVault"}
|
||||
|
||||
|
||||
@app.post("/api/pastes")
|
||||
async def create_paste(paste: PasteCreate):
|
||||
if not paste.content.strip():
|
||||
raise HTTPException(status_code=400, detail="Content cannot be empty")
|
||||
expires_at = get_expiry(paste.expiry)
|
||||
password_hash = hash_password(paste.password) if paste.password else None
|
||||
async with db_pool.acquire() as conn:
|
||||
row = await conn.fetchrow("""
|
||||
INSERT INTO pastes (title, content, language, password_hash, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, title, language, expires_at, created_at
|
||||
""", paste.title, paste.content, paste.language, password_hash, expires_at)
|
||||
return {
|
||||
"id": str(row["id"]),
|
||||
"title": row["title"],
|
||||
"language": row["language"],
|
||||
"expires_at": row["expires_at"].isoformat() if row["expires_at"] else None,
|
||||
"created_at": row["created_at"].isoformat(),
|
||||
"has_password": password_hash is not None,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/pastes/{paste_id}")
|
||||
async def get_paste(paste_id: str, password: Optional[str] = Query(default=None)):
|
||||
try:
|
||||
pid = uuid.UUID(paste_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid paste ID")
|
||||
|
||||
cached = await redis_client.get(f"paste:{paste_id}")
|
||||
if cached:
|
||||
paste = json.loads(cached)
|
||||
else:
|
||||
async with db_pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT * FROM pastes WHERE id = $1", pid)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Paste not found")
|
||||
paste = dict(row)
|
||||
paste["id"] = str(paste["id"])
|
||||
paste["expires_at"] = paste["expires_at"].isoformat() if paste["expires_at"] else None
|
||||
paste["created_at"] = paste["created_at"].isoformat()
|
||||
await redis_client.setex(f"paste:{paste_id}", 30, json.dumps(paste, default=str))
|
||||
|
||||
if paste["expires_at"]:
|
||||
if datetime.fromisoformat(paste["expires_at"]) < datetime.utcnow():
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("DELETE FROM pastes WHERE id = $1", pid)
|
||||
await redis_client.delete(f"paste:{paste_id}")
|
||||
raise HTTPException(status_code=404, detail="Paste has expired")
|
||||
|
||||
if paste["password_hash"]:
|
||||
if not password:
|
||||
return {"id": paste["id"], "title": paste["title"], "requires_password": True}
|
||||
if hash_password(password) != paste["password_hash"]:
|
||||
raise HTTPException(status_code=403, detail="Wrong password")
|
||||
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("UPDATE pastes SET views = views + 1 WHERE id = $1", pid)
|
||||
await redis_client.delete(f"paste:{paste_id}")
|
||||
|
||||
return {
|
||||
"id": paste["id"],
|
||||
"title": paste["title"],
|
||||
"content": paste["content"],
|
||||
"language": paste["language"],
|
||||
"expires_at": paste["expires_at"],
|
||||
"created_at": paste["created_at"],
|
||||
"views": paste["views"] + 1,
|
||||
"has_password": paste["password_hash"] is not None,
|
||||
"requires_password": False,
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/api/pastes/{paste_id}")
|
||||
async def delete_paste(paste_id: str, password: Optional[str] = Query(default=None)):
|
||||
try:
|
||||
pid = uuid.UUID(paste_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid paste ID")
|
||||
async with db_pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT * FROM pastes WHERE id = $1", pid)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Paste not found")
|
||||
if row["password_hash"] and (not password or hash_password(password) != row["password_hash"]):
|
||||
raise HTTPException(status_code=403, detail="Wrong password")
|
||||
async with db_pool.acquire() as conn:
|
||||
await conn.execute("DELETE FROM pastes WHERE id = $1", pid)
|
||||
await redis_client.delete(f"paste:{paste_id}")
|
||||
return {"message": "Paste deleted successfully"}
|
||||
5
SK1/backend/requirements.txt
Normal file
5
SK1/backend/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.0
|
||||
asyncpg==0.29.0
|
||||
redis==5.0.4
|
||||
pydantic==2.7.1
|
||||
34
SK1/docker-compose.yml
Normal file
34
SK1/docker-compose.yml
Normal file
@ -0,0 +1,34 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
container_name: pastevault-frontend
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: pastevault-backend
|
||||
restart: always
|
||||
expose:
|
||||
- "8000"
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: pastevault-redis
|
||||
restart: always
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
20
SK1/frontend/Dockerfile
Normal file
20
SK1/frontend/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
# Stage 1 — Build React app
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2 — Serve with Nginx
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
13
SK1/frontend/index.html
Normal file
13
SK1/frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PasteVault — Secure Code Sharing</title>
|
||||
<meta name="description" content="Securely share code and text with expiring links and password protection." />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
31
SK1/frontend/nginx.conf
Normal file
31
SK1/frontend/nginx.conf
Normal file
@ -0,0 +1,31 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Serve React app — handle client-side routing
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Proxy all /api/* requests to FastAPI backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Access logs — viewable with: docker logs pastevault-frontend
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
}
|
||||
20
SK1/frontend/package.json
Normal file
20
SK1/frontend/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "pastevault",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"highlight.js": "^11.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"vite": "^5.2.12"
|
||||
}
|
||||
}
|
||||
31
SK1/frontend/src/App.jsx
Normal file
31
SK1/frontend/src/App.jsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { Routes, Route, Link } from 'react-router-dom'
|
||||
import CreatePaste from './CreatePaste'
|
||||
import ViewPaste from './ViewPaste'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="app">
|
||||
<header>
|
||||
<Link to="/" className="logo">
|
||||
<span className="logo-icon">🔐</span>
|
||||
PasteVault
|
||||
</Link>
|
||||
<nav>
|
||||
<Link to="/" className="nav-btn">+ New Paste</Link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="/" element={<CreatePaste />} />
|
||||
<Route path="/paste/:id" element={<ViewPaste />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>PasteVault — Secure, expiring code sharing — Powered by Azure + Docker</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
SK1/frontend/src/CreatePaste.jsx
Normal file
127
SK1/frontend/src/CreatePaste.jsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const LANGUAGES = [
|
||||
'plaintext', 'bash', 'cpp', 'css', 'dockerfile',
|
||||
'go', 'html', 'java', 'javascript', 'json',
|
||||
'python', 'rust', 'sql', 'typescript', 'yaml',
|
||||
]
|
||||
|
||||
const EXPIRY_OPTIONS = [
|
||||
{ value: '', label: '♾ Never' },
|
||||
{ value: '1h', label: '⏱ 1 Hour' },
|
||||
{ value: '1d', label: '📅 1 Day' },
|
||||
{ value: '7d', label: '📅 7 Days' },
|
||||
{ value: '30d', label: '📅 30 Days' },
|
||||
]
|
||||
|
||||
export default function CreatePaste() {
|
||||
const navigate = useNavigate()
|
||||
const [form, setForm] = useState({
|
||||
title: '', content: '', language: 'plaintext', password: '', expiry: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const set = (key, val) => setForm(f => ({ ...f, [key]: val }))
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!form.content.trim()) { setError('Content cannot be empty.'); return }
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await fetch('/api/pastes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: form.title || 'Untitled',
|
||||
content: form.content,
|
||||
language: form.language,
|
||||
password: form.password || null,
|
||||
expiry: form.expiry || null,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || 'Server error')
|
||||
}
|
||||
const data = await res.json()
|
||||
navigate(`/paste/${data.id}`)
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to create paste.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="hero">
|
||||
<h1>Share code securely.</h1>
|
||||
<p>Create an expiring, password-protected paste in seconds.</p>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-row">
|
||||
<div className="form-group" style={{ flex: 2 }}>
|
||||
<label htmlFor="title">Title</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
placeholder="Untitled paste"
|
||||
value={form.title}
|
||||
onChange={e => set('title', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="language">Language</label>
|
||||
<select id="language" value={form.language} onChange={e => set('language', e.target.value)}>
|
||||
{LANGUAGES.map(l => <option key={l} value={l}>{l}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="expiry">Expires</label>
|
||||
<select id="expiry" value={form.expiry} onChange={e => set('expiry', e.target.value)}>
|
||||
{EXPIRY_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="content">Content</label>
|
||||
<textarea
|
||||
id="content"
|
||||
placeholder="Paste your code or text here..."
|
||||
value={form.content}
|
||||
onChange={e => set('content', e.target.value)}
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row form-footer">
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label htmlFor="password">Password protection (optional)</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Leave empty for public paste"
|
||||
value={form.password}
|
||||
onChange={e => set('password', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="submit-area">
|
||||
<button type="submit" className="btn-primary" disabled={loading}>
|
||||
{loading ? 'Creating…' : '🔒 Create Paste'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
167
SK1/frontend/src/ViewPaste.jsx
Normal file
167
SK1/frontend/src/ViewPaste.jsx
Normal file
@ -0,0 +1,167 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github-dark.css'
|
||||
|
||||
export default function ViewPaste() {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const codeRef = useRef(null)
|
||||
|
||||
const [paste, setPaste] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [requiresPassword, setRequiresPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [pwdError, setPwdError] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [shareUrl] = useState(window.location.href)
|
||||
const [urlCopied, setUrlCopied] = useState(false)
|
||||
|
||||
const fetchPaste = async (pwd = '') => {
|
||||
try {
|
||||
const url = pwd
|
||||
? `/api/pastes/${id}?password=${encodeURIComponent(pwd)}`
|
||||
: `/api/pastes/${id}`
|
||||
const res = await fetch(url)
|
||||
const data = await res.json()
|
||||
|
||||
if (res.status === 404) { setError(data.detail || 'Paste not found or expired.'); return }
|
||||
if (res.status === 403) { setPwdError('Wrong password. Try again.'); return }
|
||||
if (data.requires_password) { setRequiresPassword(true); return }
|
||||
|
||||
setPaste(data)
|
||||
setRequiresPassword(false)
|
||||
} catch {
|
||||
setError('Failed to load paste.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { fetchPaste() }, [id])
|
||||
|
||||
useEffect(() => {
|
||||
if (paste && codeRef.current) {
|
||||
delete codeRef.current.dataset.highlighted
|
||||
hljs.highlightElement(codeRef.current)
|
||||
}
|
||||
}, [paste])
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(paste.content)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const handleShareCopy = () => {
|
||||
navigator.clipboard.writeText(shareUrl)
|
||||
setUrlCopied(true)
|
||||
setTimeout(() => setUrlCopied(false), 2000)
|
||||
}
|
||||
|
||||
const handlePwdSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
setPwdError('')
|
||||
setLoading(true)
|
||||
fetchPaste(password)
|
||||
}
|
||||
|
||||
const formatDate = (iso) => new Date(iso).toLocaleString()
|
||||
|
||||
const formatExpiry = (iso) => {
|
||||
if (!iso) return 'Never'
|
||||
const diff = new Date(iso) - Date.now()
|
||||
if (diff < 0) return 'Expired'
|
||||
const h = Math.floor(diff / 3600000)
|
||||
if (h < 1) return `${Math.floor(diff / 60000)}m remaining`
|
||||
if (h < 24) return `${h}h remaining`
|
||||
return `${Math.floor(h / 24)}d remaining`
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="container">
|
||||
<div className="card center-card">
|
||||
<div className="spinner" />
|
||||
<p className="muted">Loading paste…</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (requiresPassword) return (
|
||||
<div className="container">
|
||||
<div className="card center-card">
|
||||
<div className="lock-icon">🔐</div>
|
||||
<h2>Password Required</h2>
|
||||
<p className="muted" style={{ marginBottom: 20 }}>This paste is password protected.</p>
|
||||
{pwdError && <div className="alert alert-error">{pwdError}</div>}
|
||||
<form onSubmit={handlePwdSubmit}>
|
||||
<div className="form-group">
|
||||
<input type="password" placeholder="Enter password" value={password}
|
||||
onChange={e => setPassword(e.target.value)} autoFocus />
|
||||
</div>
|
||||
<button type="submit" className="btn-primary" style={{ width: '100%' }}>
|
||||
Unlock Paste
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (error) return (
|
||||
<div className="container">
|
||||
<div className="card center-card">
|
||||
<div className="lock-icon">💨</div>
|
||||
<h2>Not Found</h2>
|
||||
<p className="muted" style={{ marginBottom: 20 }}>{error}</p>
|
||||
<button onClick={() => navigate('/')} className="btn-primary">Create New Paste</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!paste) return null
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="card">
|
||||
<div className="paste-header">
|
||||
<div className="paste-info">
|
||||
<h1 className="paste-title">{paste.title}</h1>
|
||||
<div className="paste-meta">
|
||||
<span>📅 {formatDate(paste.created_at)}</span>
|
||||
<span>⏳ {formatExpiry(paste.expires_at)}</span>
|
||||
<span>👁 {paste.views} {paste.views === 1 ? 'view' : 'views'}</span>
|
||||
{paste.has_password && <span>🔒 Protected</span>}
|
||||
<span className="lang-badge">{paste.language}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="paste-actions">
|
||||
<button onClick={handleShareCopy} className="btn-secondary">
|
||||
{urlCopied ? '✅ Link copied!' : '🔗 Share'}
|
||||
</button>
|
||||
<button onClick={handleCopy} className="btn-secondary">
|
||||
{copied ? '✅ Copied!' : '📋 Copy code'}
|
||||
</button>
|
||||
<button onClick={() => navigate('/')} className="btn-primary">
|
||||
+ New Paste
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="code-toolbar">
|
||||
<span className="muted" style={{ fontSize: '0.75rem' }}>
|
||||
{paste.content.split('\n').length} lines · {paste.content.length} chars
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="code-wrapper">
|
||||
<pre>
|
||||
<code ref={codeRef} className={`language-${paste.language}`}>
|
||||
{paste.content}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
302
SK1/frontend/src/index.css
Normal file
302
SK1/frontend/src/index.css
Normal file
@ -0,0 +1,302 @@
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--surface2: #21262d;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--muted: #7d8590;
|
||||
--accent: #7c3aed;
|
||||
--accent-hover: #6d28d9;
|
||||
--accent-light: rgba(124, 58, 237, 0.15);
|
||||
--danger: #f85149;
|
||||
--danger-bg: rgba(248, 81, 73, 0.1);
|
||||
--success: #3fb950;
|
||||
--radius: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ---- LAYOUT ---- */
|
||||
.app { display: flex; flex-direction: column; min-height: 100vh; }
|
||||
|
||||
header {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 28px;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.logo-icon { font-size: 1.2rem; }
|
||||
|
||||
.nav-btn {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: 7px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.nav-btn:hover { background: var(--accent-hover); }
|
||||
|
||||
main { flex: 1; padding: 28px 16px; }
|
||||
|
||||
footer {
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 14px 28px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* ---- CONTAINER & CARD ---- */
|
||||
.container { max-width: 980px; margin: 0 auto; }
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 20px 0 28px;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #e6edf3, #7c3aed);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.hero p { color: var(--muted); font-size: 1rem; }
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.center-card {
|
||||
max-width: 420px;
|
||||
margin: 60px auto;
|
||||
text-align: center;
|
||||
}
|
||||
.center-card h2 { font-size: 1.3rem; margin-bottom: 6px; }
|
||||
.lock-icon { font-size: 3rem; margin-bottom: 12px; }
|
||||
.spinner {
|
||||
width: 36px; height: 36px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ---- FORMS ---- */
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
padding: 9px 13px;
|
||||
font-size: 0.9rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-light);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 0.85rem;
|
||||
resize: vertical;
|
||||
line-height: 1.65;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.form-row .form-group { flex: 1; min-width: 140px; }
|
||||
|
||||
.form-footer {
|
||||
margin-top: 4px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.submit-area {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ---- BUTTONS ---- */
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 22px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-primary:active { transform: scale(0.98); }
|
||||
.btn-primary:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface2);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-secondary:hover { background: var(--border); }
|
||||
|
||||
/* ---- ALERTS ---- */
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.alert-error {
|
||||
background: var(--danger-bg);
|
||||
border: 1px solid rgba(248, 81, 73, 0.35);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* ---- PASTE VIEW ---- */
|
||||
.paste-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.paste-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.paste-meta {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lang-badge {
|
||||
background: var(--accent-light);
|
||||
color: #a78bfa;
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 5px;
|
||||
padding: 1px 8px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.paste-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.code-toolbar {
|
||||
padding: 6px 0 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.code-wrapper pre { margin: 0; overflow-x: auto; }
|
||||
|
||||
.code-wrapper code {
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace !important;
|
||||
font-size: 0.84rem !important;
|
||||
}
|
||||
|
||||
.hljs { background: #0d1117 !important; padding: 20px !important; }
|
||||
|
||||
.muted { color: var(--muted); }
|
||||
|
||||
/* ---- RESPONSIVE ---- */
|
||||
@media (max-width: 640px) {
|
||||
.form-row { flex-direction: column; }
|
||||
.paste-header { flex-direction: column; }
|
||||
.paste-actions { width: 100%; justify-content: flex-start; }
|
||||
.hero h1 { font-size: 1.5rem; }
|
||||
}
|
||||
11
SK1/frontend/src/main.jsx
Normal file
11
SK1/frontend/src/main.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
)
|
||||
11
SK1/frontend/vite.config.js
Normal file
11
SK1/frontend/vite.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8000'
|
||||
}
|
||||
}
|
||||
})
|
||||
73
SK1/scripts/backup.sh
Normal file
73
SK1/scripts/backup.sh
Normal file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# =============================================================
|
||||
# PasteVault - backup.sh
|
||||
# Dumps the PostgreSQL database and uploads to Azure Blob Storage.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - postgresql-client installed (sudo apt install postgresql-client)
|
||||
# - Azure CLI installed and logged in
|
||||
# - .env file present with DB_PASS set
|
||||
#
|
||||
# Usage:
|
||||
# cd sk1/
|
||||
# bash scripts/backup.sh
|
||||
# =============================================================
|
||||
|
||||
source .env
|
||||
|
||||
DB_HOST="pastevault-db.postgres.database.azure.com"
|
||||
DB_USER="pvadmin"
|
||||
DB_NAME="pastevault"
|
||||
STORAGE_ACCOUNT="pastevaultstorage"
|
||||
CONTAINER="backups"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="pastevault_backup_${TIMESTAMP}.sql"
|
||||
TMP_PATH="/tmp/$BACKUP_FILE"
|
||||
|
||||
echo "🔄 PasteVault Backup — $TIMESTAMP"
|
||||
echo ""
|
||||
|
||||
# Check pg_dump is available
|
||||
if ! command -v pg_dump &> /dev/null; then
|
||||
echo "Installing postgresql-client..."
|
||||
sudo apt-get install -y postgresql-client -qq
|
||||
fi
|
||||
|
||||
# Dump the database
|
||||
echo "1/2 — Dumping PostgreSQL database..."
|
||||
PGPASSWORD="$DB_PASS" pg_dump \
|
||||
--host=$DB_HOST \
|
||||
--username=$DB_USER \
|
||||
--dbname=$DB_NAME \
|
||||
--no-password \
|
||||
--clean \
|
||||
--if-exists \
|
||||
--file=$TMP_PATH \
|
||||
--sslmode=require
|
||||
|
||||
echo " ✅ Dump complete: $BACKUP_FILE ($(du -sh $TMP_PATH | cut -f1))"
|
||||
|
||||
# Upload to Azure Blob Storage
|
||||
echo ""
|
||||
echo "2/2 — Uploading to Azure Blob Storage..."
|
||||
az storage blob upload \
|
||||
--account-name $STORAGE_ACCOUNT \
|
||||
--container-name $CONTAINER \
|
||||
--name $BACKUP_FILE \
|
||||
--file $TMP_PATH \
|
||||
--overwrite \
|
||||
--output none
|
||||
|
||||
rm $TMP_PATH
|
||||
|
||||
echo " ✅ Upload complete"
|
||||
echo ""
|
||||
echo "✅ Backup saved: $BACKUP_FILE"
|
||||
echo ""
|
||||
echo " List all backups:"
|
||||
echo " az storage blob list --account-name $STORAGE_ACCOUNT --container-name $CONTAINER -o table"
|
||||
echo ""
|
||||
echo " Download a backup:"
|
||||
echo " az storage blob download --account-name $STORAGE_ACCOUNT --container-name $CONTAINER --name $BACKUP_FILE --file ./restore.sql"
|
||||
172
SK1/scripts/prepare-app.sh
Normal file
172
SK1/scripts/prepare-app.sh
Normal file
@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# =============================================================
|
||||
# PasteVault - prepare-app.sh
|
||||
# Provisions all Azure infrastructure and deploys the app.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Azure CLI installed and logged in (az login)
|
||||
# - .env file present in project root (copy from .env.example)
|
||||
# - SSH key at ~/.ssh/id_rsa
|
||||
#
|
||||
# Usage:
|
||||
# cd sk1/
|
||||
# bash scripts/prepare-app.sh
|
||||
# =============================================================
|
||||
|
||||
echo "🔐 PasteVault — Starting full deployment..."
|
||||
|
||||
# ---------- CONFIGURATION ----------
|
||||
RG="pastevault-rg"
|
||||
VM_LOCATION="westeurope"
|
||||
DB_LOCATION="northeurope"
|
||||
VM_NAME="pastevault-vm"
|
||||
VM_SIZE="Standard_D2s_v3"
|
||||
VM_USER="azureuser"
|
||||
DB_SERVER="pastevault-db"
|
||||
DB_NAME="pastevault"
|
||||
DB_USER="pvadmin"
|
||||
STORAGE_ACCOUNT="pastevaultstorage"
|
||||
SSH_KEY="$HOME/.ssh/id_rsa"
|
||||
|
||||
# ---------- CHECKS ----------
|
||||
if [ ! -f .env ]; then
|
||||
echo "❌ .env file not found."
|
||||
echo " Copy .env.example to .env and fill in DB_PASS and SECRET_KEY."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source .env
|
||||
|
||||
if [ -z "$DB_PASS" ]; then
|
||||
echo "❌ DB_PASS is not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$SSH_KEY" ]; then
|
||||
echo "⚙️ No SSH key found. Generating one..."
|
||||
ssh-keygen -t rsa -b 4096 -f "$SSH_KEY" -N ""
|
||||
fi
|
||||
|
||||
# ---------- RESOURCE GROUP ----------
|
||||
echo ""
|
||||
echo "1/6 — Creating resource group '$RG'..."
|
||||
az group create \
|
||||
--name $RG \
|
||||
--location $VM_LOCATION \
|
||||
--output none
|
||||
echo " ✅ Resource group ready"
|
||||
|
||||
# ---------- VIRTUAL MACHINE ----------
|
||||
echo ""
|
||||
echo "2/6 — Creating VM ($VM_SIZE · 2 vCPU · 8GB RAM)..."
|
||||
az vm create \
|
||||
--resource-group $RG \
|
||||
--name $VM_NAME \
|
||||
--image Ubuntu2204 \
|
||||
--size $VM_SIZE \
|
||||
--location $VM_LOCATION \
|
||||
--admin-username $VM_USER \
|
||||
--ssh-key-value "$SSH_KEY.pub" \
|
||||
--public-ip-sku Standard \
|
||||
--output none
|
||||
|
||||
az vm open-port --resource-group $RG --name $VM_NAME --port 80 --priority 1001 --output none
|
||||
az vm open-port --resource-group $RG --name $VM_NAME --port 443 --priority 1002 --output none
|
||||
|
||||
VM_IP=$(az vm show -d -g $RG -n $VM_NAME --query publicIps -o tsv)
|
||||
echo " ✅ VM ready — IP: $VM_IP"
|
||||
|
||||
# ---------- POSTGRESQL ----------
|
||||
echo ""
|
||||
echo "3/6 — Creating PostgreSQL Flexible Server..."
|
||||
az postgres flexible-server create \
|
||||
--resource-group $RG \
|
||||
--name $DB_SERVER \
|
||||
--location $DB_LOCATION \
|
||||
--admin-user $DB_USER \
|
||||
--admin-password "$DB_PASS" \
|
||||
--sku-name Standard_B1ms \
|
||||
--tier Burstable \
|
||||
--storage-size 32 \
|
||||
--version 15 \
|
||||
--yes \
|
||||
--output none
|
||||
|
||||
az postgres flexible-server db create \
|
||||
--resource-group $RG \
|
||||
--server-name $DB_SERVER \
|
||||
--database-name $DB_NAME \
|
||||
--output none
|
||||
|
||||
az postgres flexible-server firewall-rule create \
|
||||
--resource-group $RG \
|
||||
--name $DB_SERVER \
|
||||
--rule-name allow-vm \
|
||||
--start-ip-address $VM_IP \
|
||||
--end-ip-address $VM_IP \
|
||||
--output none
|
||||
|
||||
echo " ✅ PostgreSQL ready — $DB_SERVER.postgres.database.azure.com"
|
||||
|
||||
# ---------- STORAGE ----------
|
||||
echo ""
|
||||
echo "4/6 — Creating storage account for backups..."
|
||||
az storage account create \
|
||||
--name $STORAGE_ACCOUNT \
|
||||
--resource-group $RG \
|
||||
--location $DB_LOCATION \
|
||||
--sku Standard_LRS \
|
||||
--kind StorageV2 \
|
||||
--output none
|
||||
|
||||
az storage container create \
|
||||
--name backups \
|
||||
--account-name $STORAGE_ACCOUNT \
|
||||
--output none
|
||||
|
||||
echo " ✅ Storage account ready"
|
||||
|
||||
# ---------- INSTALL DOCKER ON VM ----------
|
||||
echo ""
|
||||
echo "5/6 — Installing Docker on VM..."
|
||||
ssh -i $SSH_KEY -o StrictHostKeyChecking=no $VM_USER@$VM_IP << 'ENDSSH'
|
||||
sudo apt-get update -y -qq
|
||||
sudo apt-get install -y -qq ca-certificates curl gnupg
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt-get update -y -qq
|
||||
sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
sudo usermod -aG docker azureuser
|
||||
ENDSSH
|
||||
echo " ✅ Docker installed"
|
||||
|
||||
# ---------- DEPLOY APP ----------
|
||||
echo ""
|
||||
echo "6/6 — Deploying PasteVault containers..."
|
||||
ssh -i $SSH_KEY -o StrictHostKeyChecking=no $VM_USER@$VM_IP "mkdir -p ~/pastevault"
|
||||
|
||||
scp -i $SSH_KEY -o StrictHostKeyChecking=no -r \
|
||||
backend frontend docker-compose.yml .env \
|
||||
$VM_USER@$VM_IP:~/pastevault/
|
||||
|
||||
ssh -i $SSH_KEY -o StrictHostKeyChecking=no $VM_USER@$VM_IP << 'ENDSSH'
|
||||
cd ~/pastevault
|
||||
sudo docker compose build --no-cache
|
||||
sudo docker compose up -d
|
||||
sleep 8
|
||||
sudo docker compose ps
|
||||
ENDSSH
|
||||
|
||||
echo ""
|
||||
echo "✅ ====================================================="
|
||||
echo " PasteVault is live!"
|
||||
echo " URL: http://$VM_IP"
|
||||
echo ""
|
||||
echo " Next: Point your Cloudflare DNS A record to $VM_IP"
|
||||
echo " Then access via HTTPS at your domain."
|
||||
echo "======================================================="
|
||||
52
SK1/scripts/remove-app.sh
Normal file
52
SK1/scripts/remove-app.sh
Normal file
@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# =============================================================
|
||||
# PasteVault - remove-app.sh
|
||||
# Stops all containers and deletes ALL Azure resources.
|
||||
#
|
||||
# WARNING: This permanently deletes the database and all data.
|
||||
# Take a backup first with: bash scripts/backup.sh
|
||||
#
|
||||
# Usage:
|
||||
# cd sk1/
|
||||
# bash scripts/remove-app.sh
|
||||
# =============================================================
|
||||
|
||||
RG="pastevault-rg"
|
||||
VM_NAME="pastevault-vm"
|
||||
VM_USER="azureuser"
|
||||
SSH_KEY="$HOME/.ssh/id_rsa"
|
||||
|
||||
echo "⚠️ WARNING: This will permanently delete all PasteVault"
|
||||
echo " resources including the database and all paste data."
|
||||
echo ""
|
||||
read -p " Type 'yes' to confirm: " CONFIRM
|
||||
|
||||
if [ "$CONFIRM" != "yes" ]; then
|
||||
echo "Aborted. Nothing was deleted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stop containers on VM first (clean shutdown)
|
||||
echo ""
|
||||
echo "🛑 Stopping containers on VM..."
|
||||
VM_IP=$(az vm show -d -g $RG -n $VM_NAME --query publicIps -o tsv 2>/dev/null || echo "")
|
||||
if [ -n "$VM_IP" ]; then
|
||||
ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 $VM_USER@$VM_IP \
|
||||
"cd ~/pastevault && sudo docker compose down -v --remove-orphans" 2>/dev/null || true
|
||||
echo " ✅ Containers stopped"
|
||||
else
|
||||
echo " ⚠️ VM not reachable, skipping container shutdown"
|
||||
fi
|
||||
|
||||
# Delete entire resource group — removes VM, DB, storage, IPs, everything
|
||||
echo ""
|
||||
echo "🗑️ Deleting Azure resource group '$RG'..."
|
||||
echo " (This takes 2-3 minutes, running in background)"
|
||||
az group delete --name $RG --yes --no-wait
|
||||
|
||||
echo ""
|
||||
echo "✅ Teardown initiated."
|
||||
echo " All Azure resources are being deleted."
|
||||
echo " Verify with: az group list -o table"
|
||||
@ -132,3 +132,243 @@ Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
E0422 08:16:00.690760 146224 portforward.go:404] "Unhandled Error" err="error copying from local connection to remote stream: writeto tcp6 [::1]:8080->[::1]:39644: read tcp6 [::1]:8080->[::1]:39644: read: connection reset by peer"
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
E0422 08:43:28.891335 146224 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942, uid : container not running (8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942)"
|
||||
E0422 08:43:28.891347 146224 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942, uid : container not running (8f982ab356928acdf2867b50e355a28049680afccea2c3c130d494ba1dd2e942)"
|
||||
error: lost connection to pod
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): services "notes-web-service" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
error: unable to forward port because pod is not running. Current status=Pending
|
||||
port-forward crashed, retrying in 2s...
|
||||
Forwarding from 127.0.0.1:8080 -> 5000
|
||||
Forwarding from [::1]:8080 -> 5000
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
error: lost connection to pod
|
||||
port-forward crashed, retrying in 2s...
|
||||
error: error upgrading connection: unable to upgrade connection: pod does not exist
|
||||
port-forward crashed, retrying in 2s...
|
||||
Forwarding from 127.0.0.1:8080 -> 5000
|
||||
Forwarding from [::1]:8080 -> 5000
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
E0422 11:16:27.685596 193095 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc, uid : container not running (0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc)"
|
||||
E0422 11:16:27.686186 193095 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod 0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc, uid : container not running (0ff84adf12ccacc61cef4f388e65da09d877fa8fc41c1e535bbe39f1e49b5ecc)"
|
||||
error: lost connection to pod
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
error: unable to forward port because pod is not running. Current status=Pending
|
||||
port-forward crashed, retrying in 2s...
|
||||
Forwarding from 127.0.0.1:8080 -> 5000
|
||||
Forwarding from [::1]:8080 -> 5000
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
E0422 11:17:42.409430 195920 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8080 -> 5000: error forwarding port 5000 to pod bc2514b8244d5e4a40432c5491da27757c33408c656764e5b3c1ae8c01b3f940, uid : container not running (bc2514b8244d5e4a40432c5491da27757c33408c656764e5b3c1ae8c01b3f940)"
|
||||
error: lost connection to pod
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): namespaces "notes-app" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): services "notes-web-service" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Error from server (NotFound): services "notes-web-service" not found
|
||||
port-forward crashed, retrying in 2s...
|
||||
Forwarding from 127.0.0.1:8080 -> 5000
|
||||
Forwarding from [::1]:8080 -> 5000
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
Handling connection for 8080
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if command -v minikube &>/dev/null; then
|
||||
echo "==> Minikube detected. Opening service URL..."
|
||||
minikube service notes-web-service -n notes-app --url
|
||||
else
|
||||
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
|
||||
echo "==> App available at: http://${NODE_IP}:30080"
|
||||
fi
|
||||
Loading…
Reference in New Issue
Block a user