Skip to content

Commit cb00a8a

Browse files
[RTY-32-260032-handle-invalid-and-valid-url]: Merge pull request #31 from recursivezero/develop
Develop
1 parent 56704a0 commit cb00a8a

23 files changed

+2593
-1954
lines changed

.env.sample

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ MODE=local
22
MONGO_URI=mongodb://<username>:<password>@127.0.0.1:27017/?authSource=admin&retryWrites=true&w=majority
33
DOMAIN=https://localhost:8001
44
PORT=8001
5-
API_VERSION=""
6-
APP_NAMe="LOCAL"
5+
API_VERSION="/api/v1"
6+
APP_NAME="LOCAL"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ poetry.lock
5959
*.tmp
6060
*.temp
6161
*.bak
62+
63+
64+
assets/images/qr/*

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,62 @@ pip install dist/*.whl
268268
pip install --upgrade dist/*.whl
269269
```
270270

271+
# 📡 Endpoints
272+
273+
# 🔐 Cache Admin Endpoints (Authentication)
274+
275+
To use the cache admin endpoints (`/cache/purge`, `/cache/remove`), you must configure a secret token in your environment and send it in the request header.
276+
Setup
277+
278+
Add a token in your .env file:
279+
280+
```
281+
CACHE_PURGE_TOKEN=your-secret-token
282+
```
283+
284+
🧪 How to test
285+
286+
PowerShell
287+
288+
```
289+
Invoke-RestMethod `
290+
-Method DELETE `
291+
-Uri "http://127.0.0.1:8000/cache/purge" `
292+
-Headers @{ "X-Cache-Token" = "your-secret-token" }
293+
```
294+
295+
🧹 Remove a single cache entry
296+
297+
```
298+
Invoke-RestMethod `
299+
-Method PATCH `
300+
-Uri "http://127.0.0.1:8000/cache/remove?key=abc123" `
301+
-Headers @{ "X-Cache-Token" = "your-secret-token" }
302+
```
303+
304+
🖥️ UI Endpoints
305+
306+
| Method | Path | Description |
307+
| ------ | --------------- | ------------------------------------ |
308+
| GET | `/` | Home page (URL shortener UI) |
309+
| GET | `/recent` | Shows recently shortened URLs |
310+
| GET | `/{short_code}` | Redirects to the original URL |
311+
| GET | `/cache/list` | 🔧 Debug cache view (local/dev only) |
312+
| DELETE | `/cache/purge` | 🧹 Remove all entries from cache |
313+
| PATCH | `/cache/remove` | 🧹 Remove a single cache entry |
314+
315+
🔌 API Endpoints (v1)
316+
317+
| Method | Path | Description |
318+
| ------ | ------------------- | ------------------------------------ |
319+
| POST | `/api/v1/shorten` | Create a short URL |
320+
| GET | `/api/v1/version` | Get API version |
321+
| GET | `/api/v1/health` | Health check (DB + cache status) |
322+
| GET | `/api/{short_code}` | Redirect to original URL |
323+
| GET | `/cache/list` | 🔧 Debug cache view (local/dev only) |
324+
| DELETE | `/cache/purge` | 🧹 Remove all entries from cache |
325+
| PATCH | `/cache/remove` | 🧹 Remove a single cache entry |
326+
271327
## License
272328

273329
📜Docs

app/api/fast_api.py

Lines changed: 8 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,19 @@
1-
import os
2-
import re
31
import traceback
4-
from datetime import datetime, timezone
5-
from typing import TYPE_CHECKING
6-
7-
from fastapi import APIRouter, FastAPI, Request
8-
from fastapi.responses import HTMLResponse, JSONResponse
9-
from pydantic import BaseModel, Field
10-
11-
if TYPE_CHECKING:
12-
from pymongo.errors import PyMongoError
13-
else:
14-
try:
15-
from pymongo.errors import PyMongoError
16-
except ImportError:
17-
18-
class PyMongoError(Exception):
19-
pass
20-
2+
from fastapi import FastAPI, Request
3+
from fastapi.responses import JSONResponse
214

225
from app import __version__
23-
from app.utils import db
24-
from app.utils.cache import get_short_from_cache, set_cache_pair
25-
from app.utils.helper import generate_code, is_valid_url, sanitize_url
26-
27-
SHORT_CODE_PATTERN = re.compile(r"^[A-Za-z0-9]{6}$")
28-
MAX_URL_LENGTH = 2048
6+
from app.routes import api_router, ui_router
297

308
app = FastAPI(
319
title="Tiny API",
3210
version=__version__,
3311
description="Tiny URL Shortener API built with FastAPI",
12+
docs_url="/docs",
13+
redoc_url="/redoc",
14+
openapi_url="/openapi.json",
3415
)
3516

36-
api_v1 = APIRouter(prefix=os.getenv("API_VERSION", "/api/v1"), tags=["v1"])
37-
3817

3918
@app.exception_handler(Exception)
4019
async def global_exception_handler(request: Request, exc: Exception):
@@ -45,167 +24,5 @@ async def global_exception_handler(request: Request, exc: Exception):
4524
)
4625

4726

48-
class ShortenRequest(BaseModel):
49-
url: str = Field(..., examples=["https://abcdkbd.com"])
50-
51-
52-
class ShortenResponse(BaseModel):
53-
success: bool = True
54-
input_url: str
55-
short_code: str
56-
created_on: datetime
57-
58-
59-
class ErrorResponse(BaseModel):
60-
success: bool = False
61-
error: str
62-
input_url: str
63-
message: str
64-
65-
66-
class VersionResponse(BaseModel):
67-
version: str
68-
69-
70-
# -------------------------------------------------
71-
# Home
72-
# -------------------------------------------------
73-
@app.get("/", response_class=HTMLResponse, tags=["Home"])
74-
async def read_root(_: Request):
75-
return """
76-
<html>
77-
<head>
78-
<title>🌙 tiny API 🌙</title>
79-
<style>
80-
body {
81-
margin: 0;
82-
height: 100vh;
83-
display: flex;
84-
align-items: center;
85-
justify-content: center;
86-
background: linear-gradient(180deg, #0b1220, #050b14);
87-
font-family: "Poppins", system-ui, Arial, sans-serif;
88-
color: #f8fafc;
89-
}
90-
.card {
91-
background: rgba(255, 255, 255, 0.06);
92-
backdrop-filter: blur(12px);
93-
border-radius: 16px;
94-
padding: 50px 40px;
95-
text-align: center;
96-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
97-
max-width: 520px;
98-
width: 90%;
99-
}
100-
h1 {
101-
font-size: 2.8em;
102-
margin-bottom: 12px;
103-
background: linear-gradient(90deg, #5ab9ff, #4cb39f);
104-
-webkit-background-clip: text;
105-
-webkit-text-fill-color: transparent;
106-
}
107-
p {
108-
font-size: 1.1em;
109-
color: #cbd5e1;
110-
margin-bottom: 30px;
111-
}
112-
a {
113-
display: inline-block;
114-
padding: 14px 26px;
115-
border-radius: 12px;
116-
background: linear-gradient(90deg, #4cb39f, #5ab9ff);
117-
color: #fff;
118-
text-decoration: none;
119-
font-weight: 700;
120-
}
121-
</style>
122-
</head>
123-
<body>
124-
<div class="card">
125-
<h1>🚀 tiny API</h1>
126-
<p>FastAPI backend for the Tiny URL shortener</p>
127-
<a href="/docs">View API Documentation</a>
128-
</div>
129-
</body>
130-
</html>
131-
"""
132-
133-
134-
@api_v1.post("/shorten", response_model=ShortenResponse, status_code=201)
135-
def shorten_url(payload: ShortenRequest):
136-
print(" SHORTEN ENDPOINT HIT ", payload.url)
137-
raw_url = payload.url.strip()
138-
139-
if len(raw_url) > MAX_URL_LENGTH:
140-
return JSONResponse(
141-
status_code=413, content={"success": False, "input_url": payload.url}
142-
)
143-
144-
original_url = sanitize_url(raw_url)
145-
146-
if not is_valid_url(original_url):
147-
return JSONResponse(
148-
status_code=400,
149-
content={
150-
"success": False,
151-
"error": "INVALID_URL",
152-
"input_url": payload.url,
153-
"message": "Invalid URL",
154-
},
155-
)
156-
157-
if db.collection is None:
158-
cached_short = get_short_from_cache(original_url)
159-
short_code = cached_short or generate_code()
160-
set_cache_pair(short_code, original_url)
161-
return {
162-
"success": True,
163-
"input_url": original_url,
164-
"short_code": short_code,
165-
"created_on": datetime.now(timezone.utc),
166-
}
167-
168-
try:
169-
existing = db.collection.find_one({"original_url": original_url})
170-
except PyMongoError:
171-
existing = None
172-
173-
if existing:
174-
return {
175-
"success": True,
176-
"input_url": original_url,
177-
"short_code": existing["short_code"],
178-
"created_on": existing["created_at"],
179-
}
180-
181-
short_code = generate_code()
182-
try:
183-
db.collection.insert_one(
184-
{
185-
"short_code": short_code,
186-
"original_url": original_url,
187-
"created_at": datetime.now(timezone.utc),
188-
}
189-
)
190-
except PyMongoError:
191-
pass
192-
193-
return {
194-
"success": True,
195-
"input_url": original_url,
196-
"short_code": short_code,
197-
"created_on": datetime.now(timezone.utc),
198-
}
199-
200-
201-
@app.get("/version")
202-
def api_version():
203-
return {"version": __version__}
204-
205-
206-
@api_v1.get("/help")
207-
def get_help():
208-
return {"message": "Welcome to Tiny API. Visit /docs for API documentation."}
209-
210-
211-
app.include_router(api_v1)
27+
app.include_router(api_router)
28+
app.include_router(ui_router)

0 commit comments

Comments
 (0)