Skip to content

Commit e23b917

Browse files
committed
Implement JWT authentication and user registration/login endpoints in Planventure API
1 parent 24c09b5 commit e23b917

11 files changed

Lines changed: 199 additions & 0 deletions

File tree

planventure-api/app.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from flask import Flask, jsonify
22
from flask_cors import CORS
33
from flask_sqlalchemy import SQLAlchemy
4+
from flask_jwt_extended import JWTManager
45
from os import environ
56
from dotenv import load_dotenv
7+
from datetime import timedelta
68

79
# Load environment variables
810
load_dotenv()
@@ -14,12 +16,44 @@ def create_app():
1416
app = Flask(__name__)
1517
CORS(app)
1618

19+
# JWT Configuration
20+
app.config['JWT_SECRET_KEY'] = environ.get('JWT_SECRET_KEY', 'your-secret-key')
21+
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
22+
jwt = JWTManager(app)
23+
24+
@jwt.expired_token_loader
25+
def expired_token_callback(jwt_header, jwt_data):
26+
return jsonify({
27+
'error': 'Token has expired',
28+
'code': 'token_expired'
29+
}), 401
30+
31+
@jwt.invalid_token_loader
32+
def invalid_token_callback(error):
33+
return jsonify({
34+
'error': 'Invalid token',
35+
'code': 'invalid_token'
36+
}), 401
37+
38+
@jwt.unauthorized_loader
39+
def missing_token_callback(error):
40+
return jsonify({
41+
'error': 'Authorization token is missing',
42+
'code': 'authorization_required'
43+
}), 401
44+
1745
# Database configuration
1846
app.config['SQLALCHEMY_DATABASE_URI'] = environ.get('DATABASE_URL', 'sqlite:///planventure.db')
1947
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
2048

2149
# Initialize extensions
2250
db.init_app(app)
51+
52+
# Register blueprints
53+
from routes.auth import auth_bp
54+
from routes.trips import trips_bp
55+
app.register_blueprint(auth_bp, url_prefix='/auth')
56+
app.register_blueprint(trips_bp, url_prefix='/api')
2357

2458
# Register routes
2559
@app.route('/')
0 Bytes
Binary file not shown.

planventure-api/middleware/auth.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from functools import wraps
2+
from flask import jsonify
3+
from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity
4+
from models import User
5+
6+
def auth_middleware(f):
7+
@wraps(f)
8+
def decorated(*args, **kwargs):
9+
try:
10+
verify_jwt_in_request(optional=True)
11+
current_user_id = get_jwt_identity()
12+
13+
# Check if user still exists in database
14+
user = User.query.get(current_user_id)
15+
if not user:
16+
return jsonify({"error": "User not found"}), 401
17+
18+
return f(*args, **kwargs)
19+
except Exception as e:
20+
return jsonify({"error": "Invalid or expired token"}), 401
21+
return decorated

planventure-api/models/user.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from datetime import datetime, timezone
2+
from flask_jwt_extended import create_access_token
23
from app import db
4+
from utils.password import hash_password, check_password
35

46
class User(db.Model):
57
__tablename__ = 'users'
@@ -13,5 +15,25 @@ class User(db.Model):
1315
# Add relationship
1416
trips = db.relationship('Trip', back_populates='user', cascade='all, delete-orphan')
1517

18+
@property
19+
def password(self):
20+
raise AttributeError('password is not a readable attribute')
21+
22+
@password.setter
23+
def password(self, password):
24+
self.password_hash = hash_password(password)
25+
26+
def verify_password(self, password):
27+
return check_password(password, self.password_hash)
28+
29+
def generate_auth_token(self):
30+
"""Generate JWT token for the user"""
31+
return create_access_token(identity=self.id)
32+
33+
@staticmethod
34+
def verify_auth_token(token):
35+
"""Verify the auth token - handled by @auth_required decorator"""
36+
pass
37+
1638
def __repr__(self):
1739
return f'<User {self.email}>'

planventure-api/routes/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .auth import auth_bp
2+
3+
__all__ = ['auth_bp']

planventure-api/routes/auth.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from flask import Blueprint, request, jsonify
2+
from app import db
3+
from models import User
4+
from utils.validators import validate_email
5+
6+
auth_bp = Blueprint('auth', __name__)
7+
8+
# Error responses
9+
INVALID_CREDENTIALS = {"error": "Invalid email or password"}, 401
10+
MISSING_FIELDS = {"error": "Missing required fields"}, 400
11+
INVALID_EMAIL = {"error": "Invalid email format"}, 400
12+
EMAIL_EXISTS = {"error": "Email already registered"}, 409
13+
14+
@auth_bp.route('/register', methods=['POST'])
15+
def register():
16+
data = request.get_json()
17+
18+
# Validate required fields
19+
if not all(k in data for k in ['email', 'password']):
20+
return jsonify(MISSING_FIELDS)
21+
22+
# Validate email format
23+
if not validate_email(data['email']):
24+
return jsonify(INVALID_EMAIL)
25+
26+
# Check if user already exists
27+
if User.query.filter_by(email=data['email']).first():
28+
return jsonify(EMAIL_EXISTS)
29+
30+
# Create new user
31+
try:
32+
user = User(email=data['email'])
33+
user.password = data['password'] # This will hash the password
34+
db.session.add(user)
35+
db.session.commit()
36+
37+
# Generate auth token
38+
token = user.generate_auth_token()
39+
return jsonify({
40+
'message': 'User registered successfully',
41+
'token': token
42+
}), 201
43+
except Exception as e:
44+
db.session.rollback()
45+
return jsonify({'error': 'Registration failed'}), 500
46+
47+
@auth_bp.route('/login', methods=['POST'])
48+
def login():
49+
data = request.get_json()
50+
51+
# Validate required fields
52+
if not all(k in data for k in ['email', 'password']):
53+
return jsonify(MISSING_FIELDS)
54+
55+
# Find user by email
56+
user = User.query.filter_by(email=data['email']).first()
57+
58+
# Verify user exists and password is correct
59+
if user and user.verify_password(data['password']):
60+
token = user.generate_auth_token()
61+
return jsonify({
62+
'message': 'Login successful',
63+
'token': token,
64+
'user': {
65+
'id': user.id,
66+
'email': user.email
67+
}
68+
}), 200
69+
70+
return jsonify(INVALID_CREDENTIALS)

planventure-api/routes/trips.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from flask import Blueprint, jsonify
2+
from middleware.auth import auth_middleware
3+
4+
trips_bp = Blueprint('trips', __name__)
5+
6+
@trips_bp.route('/trips', methods=['GET'])
7+
@auth_middleware
8+
def get_trips():
9+
# This route is now protected and will only be accessible with a valid JWT token
10+
return jsonify({"message": "Protected route accessed successfully"})

planventure-api/utils/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .password import hash_password, check_password
2+
from .auth import auth_required, get_current_user_id
3+
4+
__all__ = ['hash_password', 'check_password', 'auth_required', 'get_current_user_id']

planventure-api/utils/auth.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from functools import wraps
2+
from flask import jsonify
3+
from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity
4+
5+
def auth_required(f):
6+
@wraps(f)
7+
def decorated(*args, **kwargs):
8+
try:
9+
verify_jwt_in_request()
10+
return f(*args, **kwargs)
11+
except Exception as e:
12+
return jsonify({"msg": "Invalid token"}), 401
13+
return decorated
14+
15+
def get_current_user_id():
16+
return get_jwt_identity()

planventure-api/utils/password.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import bcrypt
2+
3+
def hash_password(password: str) -> str:
4+
"""Hash a password using bcrypt"""
5+
salt = bcrypt.gensalt()
6+
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
7+
8+
def check_password(password: str, password_hash: str) -> bool:
9+
"""Verify a password against its hash"""
10+
return bcrypt.checkpw(
11+
password.encode('utf-8'),
12+
password_hash.encode('utf-8')
13+
)

0 commit comments

Comments
 (0)