Skip to content

Commit fa5c101

Browse files
committed
Remove API key requirement and add public HTML pages
1 parent e33d4e8 commit fa5c101

7 files changed

Lines changed: 324 additions & 39 deletions

File tree

README.md

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
FastAPI application that stores uploaded files to disk, tracks them with SQLModel, and serves them back through a lightweight CDN-like interface.
44

55
## Features
6-
- Protected upload and listing endpoints secured with an API key header.
6+
- Public upload and listing endpoints with UUID-based storage.
77
- Files stored with generated UUID names while preserving the original name for metadata.
88
- Optional background cleaner that prunes files older than a configured retention window.
99
- SQLite by default, with configurable database URL via environment variables.
@@ -17,12 +17,6 @@ pip install --upgrade pip
1717
pip install -r requirements.txt
1818
```
1919

20-
Create a `.env` (or export environment variables) with at least:
21-
22-
```
23-
API_KEY=choose-a-strong-secret
24-
```
25-
2620
Optional settings:
2721

2822
| Variable | Default | Description |
@@ -43,8 +37,6 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 --proxy-headers
4337

4438
## API Overview
4539

46-
All protected endpoints require the header `x-api-key: <API_KEY>`.
47-
4840
| Method | Path | Description |
4941
|--------|------|-------------|
5042
| `POST` | `/upload` | Accepts multipart file upload, stores file, returns metadata (`id`, `url`, `size`, `type`). |
@@ -66,12 +58,9 @@ pytest
6658
```
6759

6860
The test suite spins up the FastAPI app against a temporary SQLite database to cover:
69-
- API key enforcement on uploads.
7061
- Upload/list/serve happy path.
7162
- Directory traversal hardening for file serving.
7263

7364
## Deployment Notes
74-
75-
- Ensure `API_KEY` is always set in your deployment environment—startup fails otherwise.
7665
- When using SQLite, the app configures thread-safe connection settings. For higher concurrency, consider a PostgreSQL or MySQL instance and update `DB_URL`.
7766
- Mount or back up `UPLOAD_DIR` storage if files need to persist beyond the cleaner retention window.

app/config.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33

44
load_dotenv()
55

6-
API_KEY = os.getenv("API_KEY")
7-
if not API_KEY:
8-
raise RuntimeError("API_KEY environment variable must be set")
9-
10-
UPLOAD_DIR = os.getenv("UPLOAD_DIR", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "uploads")))
6+
UPLOAD_DIR = os.getenv(
7+
"UPLOAD_DIR", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "uploads"))
8+
)
119
DB_URL = os.getenv("DB_URL", "sqlite:///./cdn.db")
1210
DELETE_AFTER_HOURS = int(os.getenv("DELETE_AFTER_HOURS", "72"))
1311
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*")

app/main.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
from fastapi import FastAPI, UploadFile, File, HTTPException, Header, Depends
2-
from fastapi.responses import FileResponse
1+
from fastapi import FastAPI, UploadFile, File, HTTPException, Request
2+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
33
from fastapi.middleware.cors import CORSMiddleware
44
from sqlmodel import SQLModel, create_engine, Session, select
55
from urllib.parse import quote
66
from pathlib import Path
7-
from app.config import API_KEY, DB_URL, UPLOAD_DIR, CORS_ORIGINS, DB_CONNECT_ARGS, ENABLE_CLEANER
7+
from app.config import DB_URL, UPLOAD_DIR, CORS_ORIGINS, DB_CONNECT_ARGS, ENABLE_CLEANER
88
from app.models import File as FileModel
99
from app.storage import save_file
1010
from app.cleaner import start_cleaner
11+
from starlette.exceptions import HTTPException as StarletteHTTPException
1112

1213
app = FastAPI(title="AlterBase CDN API", version="1.0")
1314

@@ -25,14 +26,38 @@
2526
if ENABLE_CLEANER:
2627
start_cleaner(engine) # background scheduler
2728

28-
# --- API Key guard ---
29-
async def require_api_key(x_api_key: str | None = Header(default=None)):
30-
if API_KEY and x_api_key != API_KEY:
31-
raise HTTPException(status_code=401, detail="Invalid or missing API Key")
29+
_ERROR_PAGE_PATH = Path(__file__).resolve().parent / "templates" / "404.html"
30+
_NOT_FOUND_HTML = _ERROR_PAGE_PATH.read_text(encoding="utf-8") if _ERROR_PAGE_PATH.is_file() else "<h1>404 - Not Found</h1>"
31+
32+
_HOME_PAGE_PATH = Path(__file__).resolve().parent / "templates" / "index.html"
33+
_HOME_HTML = _HOME_PAGE_PATH.read_text(encoding="utf-8") if _HOME_PAGE_PATH.is_file() else "<h1>Welcome</h1>"
34+
35+
_API_PAGE_PATH = Path(__file__).resolve().parent / "templates" / "api.html"
36+
_API_HTML = _API_PAGE_PATH.read_text(encoding="utf-8") if _API_PAGE_PATH.is_file() else "<h1>API</h1>"
37+
38+
39+
@app.exception_handler(404)
40+
async def not_found_handler(request: Request, exc: StarletteHTTPException):
41+
accept = request.headers.get("accept", "")
42+
if "text/html" in accept or "*/*" in accept or not accept:
43+
return HTMLResponse(content=_NOT_FOUND_HTML, status_code=404)
44+
detail = exc.detail if hasattr(exc, "detail") else "Not Found"
45+
return JSONResponse({"detail": detail}, status_code=404)
46+
47+
# --- Pages ---
48+
49+
@app.get("/", response_class=HTMLResponse)
50+
async def home():
51+
return HTMLResponse(content=_HOME_HTML)
52+
53+
54+
@app.get("/api-info", response_class=HTMLResponse)
55+
async def api_info():
56+
return HTMLResponse(content=_API_HTML)
3257

3358
# --- Routes ---
3459

35-
@app.post("/upload", dependencies=[Depends(require_api_key)])
60+
@app.post("/upload")
3661
async def upload(file: UploadFile = File(...)):
3762
if not file.filename:
3863
raise HTTPException(status_code=400, detail="Missing filename")
@@ -55,10 +80,10 @@ async def upload(file: UploadFile = File(...)):
5580
"id": file_id,
5681
"url": f"/{quote(stored_name)}",
5782
"size": size_bytes,
58-
"type": file.content_type,
59-
}
83+
"type": file.content_type,
84+
}
6085

61-
@app.get("/list", dependencies=[Depends(require_api_key)])
86+
@app.get("/list")
6287
def list_files():
6388
with Session(engine) as session:
6489
files = session.exec(select(FileModel).order_by(FileModel.created_at.desc())).all()

app/templates/404.html

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Not Found</title>
6+
<style>
7+
:root {
8+
color-scheme: light dark;
9+
}
10+
* {
11+
box-sizing: border-box;
12+
margin: 0;
13+
padding: 0;
14+
}
15+
body {
16+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17+
min-height: 100vh;
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
background: #0f172a;
22+
color: #f8fafc;
23+
padding: 2rem;
24+
}
25+
.card {
26+
max-width: 420px;
27+
width: 100%;
28+
background: rgba(15, 23, 42, 0.8);
29+
border: 1px solid rgba(148, 163, 184, 0.3);
30+
border-radius: 16px;
31+
padding: 2.5rem;
32+
text-align: center;
33+
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.4);
34+
}
35+
h1 {
36+
font-size: clamp(3rem, 5vw, 4rem);
37+
letter-spacing: 0.2em;
38+
margin-bottom: 1rem;
39+
opacity: 0.9;
40+
}
41+
p {
42+
font-size: 1rem;
43+
line-height: 1.6;
44+
opacity: 0.8;
45+
margin-bottom: 1.5rem;
46+
}
47+
a {
48+
display: inline-block;
49+
font-weight: 600;
50+
text-decoration: none;
51+
padding: 0.75rem 1.5rem;
52+
border-radius: 9999px;
53+
background: linear-gradient(135deg, #38bdf8, #818cf8);
54+
color: #0f172a;
55+
transition: transform 0.2s ease, box-shadow 0.2s ease;
56+
box-shadow: 0 16px 30px rgba(129, 140, 248, 0.35);
57+
}
58+
a:hover {
59+
transform: translateY(-2px);
60+
box-shadow: 0 20px 40px rgba(129, 140, 248, 0.45);
61+
}
62+
</style>
63+
</head>
64+
<body>
65+
<main class="card">
66+
<h1>404</h1>
67+
<p>The resource you were looking for isn&apos;t here. It may have been removed or its link is outdated.</p>
68+
<a href="/">Back to Home</a>
69+
</main>
70+
</body>
71+
</html>

app/templates/api.html

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>AlterBase CDN API</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<style>
8+
:root {
9+
color-scheme: light dark;
10+
}
11+
* {
12+
box-sizing: border-box;
13+
margin: 0;
14+
padding: 0;
15+
}
16+
body {
17+
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
18+
min-height: 100vh;
19+
background: #0f172a;
20+
color: #e2e8f0;
21+
padding: 3rem 1.5rem;
22+
}
23+
.container {
24+
max-width: 800px;
25+
margin: 0 auto;
26+
background: rgba(15, 23, 42, 0.75);
27+
border: 1px solid rgba(148, 163, 184, 0.25);
28+
border-radius: 18px;
29+
padding: 3rem;
30+
box-shadow: 0 30px 60px rgba(15, 23, 42, 0.4);
31+
}
32+
h1 {
33+
font-size: clamp(2.25rem, 5vw, 3rem);
34+
margin-bottom: 0.75rem;
35+
}
36+
p.lead {
37+
font-size: 1.1rem;
38+
opacity: 0.85;
39+
margin-bottom: 2rem;
40+
}
41+
section + section {
42+
margin-top: 2.5rem;
43+
}
44+
h2 {
45+
font-size: 1.35rem;
46+
margin-bottom: 1rem;
47+
color: #93c5fd;
48+
}
49+
pre {
50+
background: rgba(15, 23, 42, 0.95);
51+
border-radius: 12px;
52+
padding: 1rem 1.25rem;
53+
overflow-x: auto;
54+
font-size: 0.95rem;
55+
line-height: 1.6;
56+
border: 1px solid rgba(148, 163, 184, 0.2);
57+
}
58+
code {
59+
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
60+
"Courier New", monospace;
61+
}
62+
ul {
63+
list-style: none;
64+
margin-left: 0;
65+
}
66+
li {
67+
margin-bottom: 0.75rem;
68+
}
69+
a {
70+
color: #38bdf8;
71+
text-decoration: none;
72+
}
73+
a:hover {
74+
text-decoration: underline;
75+
}
76+
</style>
77+
</head>
78+
<body>
79+
<div class="container">
80+
<h1>AlterBase CDN API</h1>
81+
<p class="lead">Upload files and retrieve shareable URLs with a minimal HTTP API.</p>
82+
83+
<section>
84+
<h2>Base URL</h2>
85+
<pre><code>https://cdn.lunaticsm.web.id</code></pre>
86+
</section>
87+
88+
<section>
89+
<h2>Endpoints</h2>
90+
<ul>
91+
<li>
92+
<strong>POST /upload</strong><br />
93+
Multipart form request with <code>file</code> field. Returns JSON metadata with <code>id</code>,
94+
<code>url</code>, <code>size</code>, <code>type</code>.
95+
</li>
96+
<li>
97+
<strong>GET /list</strong><br />
98+
Lists stored files ordered by newest first, including original names and timestamps.
99+
</li>
100+
<li>
101+
<strong>GET /{filename}</strong><br />
102+
Serves the stored file given the UUID filename returned from <code>/upload</code>.
103+
</li>
104+
</ul>
105+
</section>
106+
107+
<section>
108+
<h2>CURL Example</h2>
109+
<pre><code>curl -X POST https://cdn.lunaticsm.web.id/upload \
110+
-F "file=@/path/to/photo.jpg" \
111+
-H "Accept: application/json"</code></pre>
112+
</section>
113+
114+
<section>
115+
<h2>Notes</h2>
116+
<ul>
117+
<li>Files are stored on disk under the configured uploads directory.</li>
118+
<li>The optional cleaner job removes files older than the configured retention window.</li>
119+
<li>Use the relative URL from <code>/upload</code> responses to build your CDN links.</li>
120+
</ul>
121+
</section>
122+
123+
<section>
124+
<a href="/">← Back to home</a>
125+
</section>
126+
</div>
127+
</body>
128+
</html>

0 commit comments

Comments
 (0)