Skip to content

Commit 424c648

Browse files
Merge pull request #21 from SAMAD101/main
feat: URL Expiration
2 parents fb415b8 + 071f03c commit 424c648

File tree

9 files changed

+128
-12
lines changed

9 files changed

+128
-12
lines changed

liberate.db

-16 KB
Binary file not shown.

pdm.lock

+26-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ dependencies = [
1616
"SQLAlchemy>=2.0.25",
1717
"psycopg2-binary>=2.9.9",
1818
"validators>=0.22.0",
19+
"aiosqlite>=0.19.0",
1920
]
2021
requires-python = ">=3.11.6"
2122
readme = "README.md"
2223
license = {text = "MIT"}
2324

2425
[tool.pdm.scripts]
25-
start = "uvicorn src.link_liberate.main:app --host 0.0.0.0 --port 8080 --workers 1 --reload"
26+
start = "uvicorn src.link_liberate.main:app --host 0.0.0.0 --port 8080 --workers 4 --reload"
2627
dev = "uvicorn src.link_liberate.main:app --host 0.0.0.0 --port 8080 --reload"
2728
test = "pytest"
2829
mypy = "mypy src/link_liberate"

requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,8 @@ virtualenv==20.25.0
5959
watchfiles==0.21.0
6060
websockets==12.0
6161
wrapt==1.16.0
62+
63+
SQLAlchemy~=2.0.25
64+
validators~=0.22.0
65+
66+
aiosqlite~=0.19.0

src/link_liberate/database.py

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
from sqlalchemy.ext.declarative import declarative_base
33
from sqlalchemy.orm import sessionmaker
44

5+
import aiosqlite
6+
import asyncio
7+
58
SQLALCHEMY_DATABASE_URL = "sqlite:///./liberate.db"
69

710
engine = create_engine(
@@ -19,3 +22,16 @@ def get_db():
1922
yield db
2023
finally:
2124
db.close()
25+
26+
27+
async def create_async_session():
28+
async with aiosqlite.connect(SQLALCHEMY_DATABASE_URL) as db:
29+
yield db
30+
31+
32+
async def expire_uuid(uuid):
33+
print("Expiring uuid:", uuid)
34+
await asyncio.sleep(60) # Default expiration time is 60 mins
35+
async with create_async_session() as db:
36+
await db.execute("DELETE FROM liberatedlinks WHERE uuid = ?", (uuid,))
37+
await db.commit()

src/link_liberate/main.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@
88
)
99
from fastapi.middleware.cors import CORSMiddleware
1010
from fastapi.templating import Jinja2Templates
11+
1112
from typing import Annotated
13+
1214
from slowapi.errors import RateLimitExceeded
1315
from slowapi import Limiter, _rate_limit_exceeded_handler
16+
1417
from slowapi.util import get_remote_address
1518
from sqlalchemy.orm import Session
19+
from asyncio import create_task
1620
from starlette.templating import _TemplateResponse
1721

1822
from .utils import generate_uuid, make_proper_url, check_link
1923
from .models import Base, LiberatedLink
20-
from .database import engine, get_db
24+
from .database import engine, get_db, expire_uuid
2125

2226
limiter = Limiter(key_func=get_remote_address)
2327
app: FastAPI = FastAPI(title="link-liberate")
@@ -38,8 +42,7 @@
3842
allow_headers=["*"],
3943
)
4044

41-
templates: Jinja2Templates = Jinja2Templates(
42-
directory=str(Path(BASE_DIR, "templates")))
45+
templates: Jinja2Templates = Jinja2Templates(directory=str(Path(BASE_DIR, "templates")))
4346

4447

4548
@app.get("/", response_class=HTMLResponse)
@@ -74,6 +77,7 @@ async def web_post(
7477
db.add(new_liberated_link)
7578
db.commit()
7679
db.refresh(new_liberated_link)
80+
await create_task(expire_uuid(uuid))
7781
context = {"link": link, "short": f"{BASE_URL}/{uuid}"}
7882
except Exception as e:
7983
raise HTTPException(
@@ -91,8 +95,7 @@ async def get_link(
9195
) -> RedirectResponse:
9296
path: str = f"data/{uuid}"
9397
try:
94-
link = db.query(LiberatedLink).filter(
95-
LiberatedLink.uuid == uuid).first()
98+
link = db.query(LiberatedLink).filter(LiberatedLink.uuid == uuid).first()
9699
return RedirectResponse(
97100
url=link.link, status_code=status.HTTP_301_MOVED_PERMANENTLY
98101
)

src/link_liberate/templates/base.html

+44-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
}
2424

2525
a {
26-
text-decoration: none !important;
26+
text-decoration: none !important;
2727
}
2828

2929
h1 {
@@ -57,6 +57,48 @@
5757
.submit-button:hover {
5858
background-color: #45a049;
5959
}
60+
61+
.tooltip {
62+
position: relative;
63+
display: inline-block;
64+
border-bottom: 1px dotted black;
65+
}
66+
67+
.tooltip .tooltiptext {
68+
visibility: hidden;
69+
width: 120px;
70+
background-color: #555;
71+
color: #fff;
72+
text-align: center;
73+
padding: 5px 0;
74+
border-radius: 6px;
75+
76+
position: absolute;
77+
z-index: 1;
78+
bottom: 125%;
79+
left: 50%;
80+
margin-left: -60px;
81+
82+
opacity: 0;
83+
transition: opacity 0.3s;
84+
}
85+
86+
.tooltip .tooltiptext::after {
87+
content: "";
88+
position: absolute;
89+
top: 100%;
90+
left: 50%;
91+
margin-left: -5px;
92+
border-width: 5px;
93+
border-style: solid;
94+
border-color: #555 transparent transparent transparent;
95+
}
96+
97+
.tooltip:hover .tooltiptext {
98+
visibility: visible;
99+
opacity: 1;
100+
}
101+
60102
.copy-button {
61103
background-color: #4CAF50;
62104
color: #fff;
@@ -85,4 +127,4 @@
85127
{% endblock %}
86128
</body>
87129

88-
</html>
130+
</html>

src/link_liberate/templates/liberate.html

+25
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,33 @@
2020
<div class="form-container">
2121
<form method="post" action="/liberate">
2222
<input type="text" name="content" class="link-input" placeholder="Enter your link...">
23+
<div class="expiration-options">
24+
<input type="checkbox" id="enable-expiration" name="enable-expiration" checked>
25+
Enable link expiration
26+
<div class="tooltip">🛈
27+
<span class="tooltiptext">The shortened link will stop working after some time.</span>
28+
</div>
29+
<select name="expiration-time">
30+
<option value="30">30 minutes</option>
31+
<option value="60">60 minutes</option>
32+
<option value="90">90 minutes</option>
33+
</select>
34+
</div>
2335
<button type="submit" class="submit-button">Shorten</button>
2436
</form>
2537
</div>
2638
</div>
39+
40+
<script>
41+
const tooltipIcon = document.querySelector('.tooltip-icon');
42+
const hiddenTooltip = document.querySelector('.hidden-tooltip');
43+
44+
tooltipIcon.addEventListener('mouseenter', () => {
45+
hiddenTooltip.classList.remove('hidden');
46+
});
47+
48+
tooltipIcon.addEventListener('mouseleave', () => {
49+
hiddenTooltip.classList.add('hidden');
50+
});
51+
</script>
2752
{% endblock %}

tests/test_main.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@ def test_web():
2121

2222
def test_web_post(mocker):
2323
# Patch the get_db function to return the db mock
24-
mocker.patch('src.link_liberate.database.Session_Local', return_value=Mock())
24+
mocker.patch("src.link_liberate.database.Session_Local", return_value=Mock())
2525
client_mock = TestClient(app)
2626
response = client_mock.post("/liberate", data={"content": "valid_content"})
2727
assert response.status_code == 200
2828

2929

30-
3130
def test_get_link(mocker):
3231
# Patch the get_db function to return the db mock
33-
mocker.patch('src.link_liberate.database.Session_Local', return_value=Mock())
32+
mocker.patch("src.link_liberate.database.Session_Local", return_value=Mock())
3433
client_mock = TestClient(app)
3534
response = client_mock.get("/valid_uuid", follow_redirects=False)
3635
assert response.status_code == 301

0 commit comments

Comments
 (0)