This commit is contained in:
Sarukesh Boominathan 2026-05-12 15:19:22 +02:00
parent 1002e93c93
commit e581827e42
36 changed files with 1759 additions and 9 deletions

269
SK1/README.md Normal file
View 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
View 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
View 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"}

View 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
View 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
View 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
View 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
View 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
View 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
View 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 &mdash; Secure, expiring code sharing &mdash; Powered by Azure + Docker</p>
</footer>
</div>
)
}

View 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>
)
}

View 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
View 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
View 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>
)

View 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
View 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
View 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
View 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"

View File

@ -132,3 +132,243 @@ Handling connection for 8080
Handling connection for 8080 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" 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

View File

@ -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