Skip to content

Commit 77b7aaa

Browse files
authored
Merge pull request #116 from spoo-me/chore/dead-code-cleanup
Chore/Oauth Refactor & Dead code Cleanup
2 parents 735ab96 + b1dceb4 commit 77b7aaa

50 files changed

Lines changed: 1141 additions & 1376 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Secrets — never bake into image layers
2+
.env
3+
.env.*
4+
!.env.example
5+
6+
# Version control
7+
.git
8+
.gitignore
9+
10+
# Python artifacts
11+
__pycache__/
12+
*.py[cod]
13+
*.so
14+
.venv
15+
.ruff_cache/
16+
.mypy_cache/
17+
.pytest_cache/
18+
.coverage
19+
.coverage.*
20+
htmlcov/
21+
22+
# Dev/test files
23+
tests/
24+
k6-tests/
25+
local_test_db/
26+
misc/
27+
thoughts/
28+
29+
# Editor/OS
30+
.vscode/
31+
.DS_Store
32+
33+
# Docs
34+
*.md
35+
!README.md

.env.example

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
# MongoDB connection details
22
MONGODB_URI="mongodb://localhost:27017/"
3-
MONGODB_URI_DEV="mongodb://localhost:27017/"
4-
MONGO_DB_NAME="url-shortener"
53

64
# Redis connection details
75
REDIS_URI="redis://localhost:6379"
8-
REDIS_URI_DEV="redis://localhost:6379"
96
REDIS_TTL_SECONDS=3600 # 1 hour
107

118
# Sentry configuration (error tracking & performance monitoring)
@@ -17,7 +14,6 @@ SENTRY_PROFILE_SAMPLE_RATE=0.05 # % of profiling sessions to capture (1.0 in
1714
# Flask configs
1815
FLASK_SECRET_KEY="" # To generate: import os; print(os.urandom(32))
1916
HOST_URI="127.0.0.1:8000"
20-
SHORTEN_API_RATE_LIMIT_PER_HOUR=100
2117
ENV="development" # change to "production" in production
2218

2319
# Logging Configuration

.github/workflows/api_test.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ jobs:
2121
uses: actions/checkout@v6
2222

2323
- name: Setup environment
24-
run: cp .env.example .env
24+
run: |
25+
cp .env.example .env
26+
echo "FLASK_SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))')" >> .env
2527
2628
- name: Start services
2729
run: docker compose up -d --build --wait
2830

2931
- name: Wait for app to be ready
3032
run: |
3133
for i in {1..30}; do
32-
if docker compose exec app uv run python -c "import requests; requests.get('http://localhost:8000/metric', timeout=2)" 2>/dev/null; then
34+
if docker compose exec app uv run python3 -c "import requests; requests.get('http://localhost:8000/metric', timeout=2)" 2>/dev/null; then
3335
echo "✅ App is ready"
3436
exit 0
3537
fi

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,6 @@ todo.md
187187
k6-tests/
188188
local_test_db/
189189

190-
.DS_Store
190+
.DS_Store
191+
192+
thoughts/

api/v1/keys.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
revoke_api_key_by_id,
1414
)
1515
from blueprints.limiter import limiter, rate_limit_key_for_request
16+
from blueprints.limits import Limits
1617

1718
from . import api_v1
1819

@@ -46,7 +47,7 @@ def _parse_expires_at(value: Optional[str | int | float]):
4647

4748

4849
@api_v1.route("/keys", methods=["POST"])
49-
@limiter.limit("5 per hour", key_func=rate_limit_key_for_request)
50+
@limiter.limit(Limits.API_KEY_CREATE, key_func=rate_limit_key_for_request)
5051
@requires_auth
5152
def create_api_key():
5253
"""
@@ -255,7 +256,7 @@ def create_api_key():
255256

256257

257258
@api_v1.route("/keys", methods=["GET"])
258-
@limiter.limit("60 per minute", key_func=rate_limit_key_for_request)
259+
@limiter.limit(Limits.API_KEY_READ, key_func=rate_limit_key_for_request)
259260
@requires_auth
260261
def list_api_keys():
261262
"""

api/v1/management.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,11 @@ def delete_url_v1(url_id: str) -> tuple[Response, int]:
272272
return jsonify({"error": "Invalid URL ID format"}), 400
273273

274274
# Validate ownership first
275-
builder = UpdateUrlRequestBuilder({}, url_id)
276-
builder.parse_auth_scope(required_scopes={"urls:manage", "admin:all"})
277-
builder.load_and_validate_ownership()
275+
builder = (
276+
UpdateUrlRequestBuilder({}, url_id)
277+
.parse_auth_scope(required_scopes={"urls:manage", "admin:all"})
278+
.load_and_validate_ownership()
279+
)
278280

279281
if builder.error:
280282
return builder.error

blueprints/auth.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pymongo.errors import DuplicateKeyError
55

66
from .limiter import limiter, rate_limit_key_for_request
7+
from .limits import Limits
78
from utils.logger import get_logger
89
from utils.auth_utils import (
910
verify_password,
@@ -42,8 +43,7 @@
4243

4344

4445
@auth.route("/auth/login", methods=["POST"])
45-
@limiter.limit("5/minute")
46-
@limiter.limit("50/day")
46+
@limiter.limit(Limits.LOGIN)
4747
def login():
4848
body = request.get_json(silent=True) or {}
4949
email = (body.get("email") or "").strip().lower()
@@ -76,7 +76,7 @@ def login():
7676

7777

7878
@auth.route("/auth/refresh", methods=["POST"])
79-
@limiter.limit("20/minute")
79+
@limiter.limit(Limits.TOKEN_REFRESH)
8080
def refresh():
8181
refresh_token = request.cookies.get("refresh_token")
8282
if not refresh_token:
@@ -122,7 +122,7 @@ def refresh():
122122

123123

124124
@auth.route("/auth/logout", methods=["POST"])
125-
@limiter.limit("60/hour")
125+
@limiter.limit(Limits.LOGOUT)
126126
def logout():
127127
user_id = getattr(g, "user_id", None)
128128
if user_id:
@@ -136,7 +136,7 @@ def logout():
136136

137137
@auth.route("/auth/me", methods=["GET"])
138138
@requires_auth
139-
@limiter.limit("60/minute", key_func=rate_limit_key_for_request)
139+
@limiter.limit(Limits.AUTH_READ, key_func=rate_limit_key_for_request)
140140
def me():
141141
user_id = g.user_id
142142
user = get_user_by_id(user_id)
@@ -146,8 +146,7 @@ def me():
146146

147147

148148
@auth.route("/auth/register", methods=["POST"])
149-
@limiter.limit("5/minute")
150-
@limiter.limit("50/day")
149+
@limiter.limit(Limits.SIGNUP)
151150
def register():
152151
body = request.get_json(silent=True) or {}
153152
email = (body.get("email") or "").strip().lower()
@@ -157,7 +156,7 @@ def register():
157156
return jsonify({"error": "email and password are required"}), 400
158157

159158
# Validate password with comprehensive checks
160-
is_valid, missing_requirements = validate_password(password)
159+
is_valid, missing_requirements, _ = validate_password(password)
161160
if not is_valid:
162161
return jsonify(
163162
{
@@ -240,7 +239,7 @@ def register():
240239

241240
@auth.route("/auth/set-password", methods=["POST"])
242241
@requires_auth
243-
@limiter.limit("5/minute")
242+
@limiter.limit(Limits.SET_PASSWORD)
244243
def set_password():
245244
"""Set password for OAuth-only users"""
246245
user = get_user_by_id(g.user_id)
@@ -257,7 +256,7 @@ def set_password():
257256
return jsonify({"error": "password is required"}), 400
258257

259258
# Validate password with comprehensive checks
260-
is_valid, missing_requirements = validate_password(password)
259+
is_valid, missing_requirements, _ = validate_password(password)
261260
if not is_valid:
262261
return jsonify(
263262
{
@@ -312,7 +311,7 @@ def register_redirect():
312311

313312
@auth.route("/auth/verify", methods=["GET"])
314313
@requires_auth
315-
@limiter.limit("60/minute", key_func=rate_limit_key_for_request)
314+
@limiter.limit(Limits.AUTH_READ, key_func=rate_limit_key_for_request)
316315
def verify_page():
317316
"""Email verification page"""
318317
user = get_user_by_id(g.user_id)
@@ -328,7 +327,7 @@ def verify_page():
328327

329328
@auth.route("/auth/send-verification", methods=["POST"])
330329
@requires_auth
331-
@limiter.limit("3/hour", key_func=rate_limit_key_for_request)
330+
@limiter.limit(Limits.RESEND_VERIFICATION, key_func=rate_limit_key_for_request)
332331
def send_verification_email():
333332
"""Send email verification OTP to authenticated user"""
334333
user = get_user_by_id(g.user_id)
@@ -379,7 +378,7 @@ def send_verification_email():
379378

380379
@auth.route("/auth/verify-email", methods=["POST"])
381380
@requires_auth
382-
@limiter.limit("10/hour", key_func=rate_limit_key_for_request)
381+
@limiter.limit(Limits.EMAIL_VERIFY, key_func=rate_limit_key_for_request)
383382
def verify_email():
384383
"""Verify email using OTP code"""
385384
user = get_user_by_id(g.user_id)
@@ -457,7 +456,7 @@ def verify_email():
457456

458457

459458
@auth.route("/auth/request-password-reset", methods=["POST"])
460-
@limiter.limit("3/hour")
459+
@limiter.limit(Limits.PASSWORD_RESET_REQUEST)
461460
def request_password_reset():
462461
"""Request password reset OTP"""
463462
body = request.get_json(silent=True) or {}
@@ -543,7 +542,7 @@ def request_password_reset():
543542

544543

545544
@auth.route("/auth/reset-password", methods=["POST"])
546-
@limiter.limit("5/hour")
545+
@limiter.limit(Limits.PASSWORD_RESET_CONFIRM)
547546
def reset_password():
548547
"""Reset password using OTP code"""
549548
body = request.get_json(silent=True) or {}
@@ -565,7 +564,7 @@ def reset_password():
565564
user_id = str(user["_id"])
566565

567566
# Validate new password
568-
is_valid, missing_requirements = validate_password(new_password)
567+
is_valid, missing_requirements, _ = validate_password(new_password)
569568
if not is_valid:
570569
return jsonify(
571570
{

blueprints/contact.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@
1010
from utils.url_utils import get_client_ip
1111
from utils.logger import get_logger
1212
from .limiter import limiter
13+
from .limits import Limits
1314

1415
contact = Blueprint("contact", __name__)
1516
log = get_logger(__name__)
1617

1718

1819
@contact.route("/contact", methods=["GET", "POST"])
19-
@limiter.limit("20/day")
20-
@limiter.limit("10/hour")
21-
@limiter.limit("3/minute")
20+
@limiter.limit(Limits.CONTACT_DAY)
21+
@limiter.limit(Limits.CONTACT_HOUR)
22+
@limiter.limit(Limits.CONTACT_MINUTE)
2223
def contact_route():
2324
if request.method == "POST":
2425
email = request.values.get("email")
@@ -88,9 +89,9 @@ def contact_route():
8889

8990

9091
@contact.route("/report", methods=["GET", "POST"])
91-
@limiter.limit("20/day")
92-
@limiter.limit("10/hour")
93-
@limiter.limit("3/minute")
92+
@limiter.limit(Limits.CONTACT_DAY)
93+
@limiter.limit(Limits.CONTACT_HOUR)
94+
@limiter.limit(Limits.CONTACT_MINUTE)
9495
def report():
9596
if request.method == "POST":
9697
# Only read from form data (POST), not query parameters

blueprints/dashboard.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
from utils.auth_utils import get_user_profile
1313
from blueprints.limiter import limiter, rate_limit_key_for_request
14+
from blueprints.limits import Limits
1415
from utils.logger import get_logger
1516

1617
log = get_logger(__name__)
@@ -20,7 +21,7 @@
2021

2122

2223
@dashboard_bp.route("/", methods=["GET"])
23-
@limiter.limit("60 per minute", key_func=rate_limit_key_for_request)
24+
@limiter.limit(Limits.DASHBOARD_READ, key_func=rate_limit_key_for_request)
2425
@requires_auth
2526
def dashboard():
2627
# Redirect to links page as the default dashboard view
@@ -29,7 +30,7 @@ def dashboard():
2930

3031
@dashboard_bp.route("/links", methods=["GET"])
3132
@requires_auth
32-
@limiter.limit("60 per minute", key_func=rate_limit_key_for_request)
33+
@limiter.limit(Limits.DASHBOARD_READ, key_func=rate_limit_key_for_request)
3334
def dashboard_links():
3435
user = get_user_by_id(g.user_id)
3536
if not user:
@@ -43,7 +44,7 @@ def dashboard_links():
4344

4445

4546
@dashboard_bp.route("/keys", methods=["GET"])
46-
@limiter.limit("60 per minute", key_func=rate_limit_key_for_request)
47+
@limiter.limit(Limits.DASHBOARD_READ, key_func=rate_limit_key_for_request)
4748
@requires_auth
4849
def dashboard_keys():
4950
user = get_user_by_id(g.user_id)
@@ -76,7 +77,7 @@ def dashboard_statistics():
7677

7778
@dashboard_bp.route("/settings", methods=["GET"])
7879
@requires_auth
79-
@limiter.limit("60 per minute", key_func=rate_limit_key_for_request)
80+
@limiter.limit(Limits.DASHBOARD_READ, key_func=rate_limit_key_for_request)
8081
def dashboard_settings():
8182
user = get_user_by_id(g.user_id)
8283
if not user:
@@ -91,7 +92,7 @@ def dashboard_settings():
9192

9293
@dashboard_bp.route("/billing", methods=["GET"])
9394
@requires_auth
94-
@limiter.limit("60 per minute", key_func=rate_limit_key_for_request)
95+
@limiter.limit(Limits.DASHBOARD_READ, key_func=rate_limit_key_for_request)
9596
def dashboard_billing():
9697
user = get_user_by_id(g.user_id)
9798
if not user:
@@ -105,7 +106,7 @@ def dashboard_billing():
105106

106107

107108
@dashboard_bp.route("/profile-pictures", methods=["GET"])
108-
@limiter.limit("30 per minute", key_func=rate_limit_key_for_request)
109+
@limiter.limit(Limits.DASHBOARD_WRITE, key_func=rate_limit_key_for_request)
109110
@requires_auth
110111
def get_profile_pictures():
111112
"""Get available profile pictures from connected OAuth providers"""
@@ -136,7 +137,7 @@ def get_profile_pictures():
136137

137138

138139
@dashboard_bp.route("/profile-pictures", methods=["POST"])
139-
@limiter.limit("5 per minute", key_func=rate_limit_key_for_request)
140+
@limiter.limit(Limits.DASHBOARD_SENSITIVE, key_func=rate_limit_key_for_request)
140141
@requires_auth
141142
def set_profile_picture():
142143
"""Set user's profile picture from available options"""

0 commit comments

Comments
 (0)