Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server Side JWT Auth #16

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/api/.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
MOVIE_API_HOST=0.0.0.0
MOVIE_API_PORT=5000
OPENWEATHER_API_KEY=9291dbdcabf7c26431129cb4438c30e1
JWT_SECRET_KEY=2df599ec6fbe45e48a0f8e0ba2a4d247

12 changes: 8 additions & 4 deletions server/api/app.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
#!/usr/bin/env python3
""" Starts the Flask web app """
import os

import requests
from dotenv import load_dotenv
from flask import Flask, jsonify, request
from models import storage
from api.views import app_views
from api.views.auth import jwt
from flask_cors import CORS
from datetime import timedelta


load_dotenv()
app = Flask(__name__)
jwt.init_app(app)
CORS(app)
app.url_map.strict_slashes = False
app.register_blueprint(app_views)
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=24)
HOST = "0.0.0.0"
PORT = 5000
SECRET_KEY = os.getenv('SECRET_KEY')


OPENWEATHER_API_KEY = os.getenv('OPENWEATHER_API_KEY')


# Weather route to be moved to api.views.weather
def get_weather(lat, lon):
base_url = "https://api.openweathermap.org/data/2.5/weather"
params = {
Expand All @@ -41,6 +43,7 @@ def get_weather(lat, lon):
return None # Return None if the request failed


# Weather route to be moved to api.views.weather
@app.route('/weather', methods=['GET'])
def weather():
lat = request.args.get('lat')
Expand All @@ -59,6 +62,7 @@ def weather():
return jsonify({'error': 'Failed to fetch weather data'}), 500


# To be removed att deployement
@app.route('/volumes')
def volume():
""" A dummy route to test volumes of docker"""
Expand Down
2 changes: 2 additions & 0 deletions server/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
app_views = Blueprint('app_views', __name__, url_prefix="/api")

from api.views.index import * # nopep8
from api.views.auth import * # nopep8
from api.views.users import * # nopep8
66 changes: 66 additions & 0 deletions server/api/views/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/python3
"""Auth routes"""
from api.views import app_views
from flask import jsonify, request
from flask_jwt_extended import JWTManager, create_access_token
from models import storage
from models.user import User


jwt = JWTManager()


@jwt.user_identity_loader
def user_identity_lookup(user):
return user.id


@jwt.user_lookup_loader
def user_lookup_callback(_jwt_header, jwt_data):
identity = jwt_data["sub"]
return storage.get(User, identity)


@app_views.route('/register', methods=['POST'])
def register_user():
"""Register a new user"""
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400

email = data.get('email')
password = data.get('password')
first_name = data.get('first_name')
last_name = data.get('last_name')

if not email or not password or not first_name or not last_name:
return jsonify({'error': 'Missing required fields'}), 400

if storage.get_specific(User, 'email', email):
return jsonify({'error': 'User already exists'}), 400

new_user = User(**data)

storage.new(new_user)
storage.save()

return jsonify(new_user.to_dict()), 201


@app_views.route('/login', methods=['POST'])
def login():
"""Sign in an user"""
data = request.get_json()
if not data or 'email' not in data or 'password' not in data:
return jsonify({'error': 'Missing email or password'}), 400

email = data['email']
password = data['password']

user = storage.get_specific(User, 'email', email)

if not user or user.verify_password(password, user.password) is False:
return jsonify({'error': 'Invalid email or password'}), 401

token = create_access_token(identity=user)
return jsonify({'token': token})
22 changes: 22 additions & 0 deletions server/api/views/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/python3
"""Users related routes"""
from api.views import app_views
from flask import jsonify, request
from flask_jwt_extended import get_jwt_identity, jwt_required
from models.user import User
from models import storage


@app_views.route('/users/profile', methods=['GET'])
@jwt_required()
def get_user_profile():
"""Get user profile"""
try:
current_user = get_jwt_identity()
except Exception as e:
return jsonify({"error": "Invalid token"}), 401

user_profile = storage.get_specific(User, 'id', current_user)
if not user_profile:
return jsonify({"error": "User not found"}), 404
return jsonify(user_profile.to_dict())
2 changes: 1 addition & 1 deletion server/flask.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ WORKDIR /movie_name

COPY . /movie_name

RUN pip3 install flask==2.1.0 werkzeug==2.1.1 flask-cors==4.0.1 sqlalchemy==1.4.22 mysqlclient==2.2.4 python-dotenv requests
RUN pip3 install flask==2.1.0 werkzeug==2.1.1 flask-cors==4.0.1 sqlalchemy==1.4.22 mysqlclient==2.2.4 python-dotenv requests flask-jwt-extended

RUN apt-get clean && rm -rf /var/lib/apt/lists/*

Expand Down
13 changes: 7 additions & 6 deletions server/models/engine/db_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Contains the class DBStorage
"""

from models.base_model import BaseModel, Base
from models.base_model import Base
from models.movie import Movie
from models.user_movie import User_Movie
from models.user import User
Expand Down Expand Up @@ -69,11 +69,12 @@ def close(self):
def get(self, cls, id):
"""Returns the object based on the class and its ID,
or None if not found"""
objs = self.all(cls).values()
for obj in objs:
if obj.id == id:
return obj
return None
return self.__session.query(cls).filter(getattr(cls, 'id') == id)

def get_specific(self, cls, attribute, value):
"""Retun the object based on the class, attrubite and value"""
return self.__session.query(cls).filter(
getattr(cls, attribute) == value).first()

def count(self, cls=None):
"""
Expand Down
9 changes: 9 additions & 0 deletions server/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ def __init__(self, *args, **kwargs):
kwargs["password"] = hashlib.sha256(
kwargs["password"].encode()).hexdigest()
super().__init__(*args, **kwargs)

@staticmethod
def verify_password(plain_password, hashed_password):
"""Verify password"""
plain_password_hash = hashlib.sha256(
plain_password.encode()).hexdigest()
if plain_password_hash == hashed_password:
return True
return False