Skip to content

Commit 55d58ec

Browse files
Merge pull request #26 from recursivezero/main
🚀 Release: 2026-02-18
2 parents 43590b1 + d550988 commit 55d58ec

27 files changed

+2605
-1990
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"

.github/actions/comment-on-issue/action.yml

Lines changed: 0 additions & 20 deletions
This file was deleted.

.github/workflows/comment-on-issue.yml

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
description: "Issue number"
1111
required: true
1212
type: number
13+
comment_body:
14+
description: "Comment text"
15+
required: true
16+
type: string
1317

1418
permissions:
1519
issues: write
@@ -20,16 +24,9 @@ jobs:
2024
steps:
2125
- uses: actions/checkout@v4
2226

23-
- uses: actions/github-script@v8
27+
- name: Run Comment Action
28+
uses: recursivezero/action-club/.github/actions/comment-on-issue@main
2429
with:
25-
script: |
26-
// For workflow_dispatch, use the input; for issue events, use context.issue.number
27-
const issue_number = context.payload.inputs?.issue_number
28-
? parseInt(context.payload.inputs.issue_number, 10)
29-
: context.issue.number;
30-
31-
await github.rest.issues.createComment({
32-
...context.repo,
33-
issue_number,
34-
body: "👋 Thank you for opening this issue! We will look into it as soon as possible."
35-
});
30+
issue_number: ${{ github.event.inputs.issue_number }}
31+
comment_body: ${{ github.event.inputs.comment_body }}
32+
github_token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/format-issue-title.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: "Auto Format Issue Title"
1+
name: "Format Issue Title"
22

33
on:
44
issues:
@@ -11,7 +11,6 @@ jobs:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- name: Format issue title
14-
uses: recursivezero/template/.github/actions/format-issue-title@v2.6
14+
uses: recursivezero/action-club/.github/actions/format-issue-title@v0.2.57
1515
with:
1616
prefix: RTY
17-
dry_run: false

.github/workflows/project-assign.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ run-name: Assign project under issue and pull requests
44
on:
55
issues:
66
types: [opened]
7-
pull_request:
7+
pull_request_target:
88
types: [opened, reopened]
99

1010
permissions:

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