add users table + authentication
This commit is contained in:
parent
6ac29ca2c3
commit
515d8e87fe
11 changed files with 311 additions and 17 deletions
51
app.py
51
app.py
|
@ -1,16 +1,49 @@
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from models import db
|
from models import db
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from routes import api
|
from routes.userRoutes import userRoutes
|
||||||
|
from routes.eventRoutes import eventRoutes
|
||||||
import config
|
import config
|
||||||
|
from flask_jwt_extended import JWTManager
|
||||||
|
|
||||||
app = Flask(__name__)
|
class App:
|
||||||
app.config.from_object(config.Config)
|
def __init__(self):
|
||||||
db.init_app(app)
|
self.app = Flask(__name__)
|
||||||
migrate = Migrate(app, db)
|
self.set_config()
|
||||||
app.register_blueprint(api)
|
self.set_up_db()
|
||||||
|
self.set_up_jwt()
|
||||||
|
self.register_blueprints()
|
||||||
|
|
||||||
|
def set_config(self):
|
||||||
|
self.app.config.from_object(config.Config)
|
||||||
|
|
||||||
|
def set_up_db(self):
|
||||||
|
db.init_app(self.app)
|
||||||
|
self.migrate = Migrate(self.app, db)
|
||||||
|
|
||||||
|
def set_up_jwt(self):
|
||||||
|
self.jwt_manager = JWTManager(self.app)
|
||||||
|
self.app.config['JWT_TOKEN_LOCATION'] = ['cookies']
|
||||||
|
self.app.config['JWT_COOKIE_NAME'] = 'access_token_cookie'
|
||||||
|
|
||||||
|
|
||||||
|
def register_blueprints(self):
|
||||||
|
self.app.register_blueprint(userRoutes, url_prefix='/user')
|
||||||
|
self.app.register_blueprint(eventRoutes, url_prefix='/event')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
with self.app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
self.app.run(debug=True)
|
||||||
|
|
||||||
|
def print_endpoints(self):
|
||||||
|
print("Endpoints and their functions:")
|
||||||
|
for rule in self.app.url_map.iter_rules():
|
||||||
|
print(f"Endpoint: {rule.endpoint}, Path: {rule}")
|
||||||
|
function_name = self.app.view_functions[rule.endpoint].__name__
|
||||||
|
print(f" Function: {function_name}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with app.app_context():
|
app_instance = App()
|
||||||
db.create_all()
|
app_instance.print_endpoints()
|
||||||
app.run(debug=True)
|
app_instance.run()
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
class Config:
|
class Config:
|
||||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///events.db'
|
SQLALCHEMY_DATABASE_URI = 'sqlite:///events.db'
|
||||||
|
JWT_SECRET_KEY = 'your_jwt_secret_key'
|
|
@ -1,6 +1,63 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import request, jsonify
|
from flask import request, jsonify
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
|
|
||||||
|
|
||||||
|
def validate_user_post_request(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"message": "No input data provided"}), 400
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
required_fields = ['username', 'password', 'email', 'location']
|
||||||
|
if not all(field in data for field in required_fields):
|
||||||
|
return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400
|
||||||
|
|
||||||
|
# Validate 'username'
|
||||||
|
if not isinstance(data['username'], str) or not data['username'].strip():
|
||||||
|
return jsonify({"message": "Invalid username"}), 400
|
||||||
|
|
||||||
|
# Validate 'password'
|
||||||
|
if not isinstance(data['password'], str) or not data['password'].strip():
|
||||||
|
return jsonify({"message": "Invalid password"}), 400
|
||||||
|
|
||||||
|
# Validate 'email'
|
||||||
|
if not isinstance(data['email'], str) or not data['email'].strip():
|
||||||
|
return jsonify({"message": "Invalid email"}), 400
|
||||||
|
|
||||||
|
# Validate 'location'
|
||||||
|
if not isinstance(data['location'], str) or not data['location'].strip():
|
||||||
|
return jsonify({"message": "Invalid location"}), 400
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def validate_user_login_request(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({"message": "No input data provided"}), 400
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
required_fields = ['email', 'password']
|
||||||
|
if not all(field in data for field in required_fields):
|
||||||
|
return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400
|
||||||
|
|
||||||
|
# Validate 'email'
|
||||||
|
if not isinstance(data['email'], str) or not data['email'].strip():
|
||||||
|
return jsonify({"message": "Invalid email"}), 400
|
||||||
|
|
||||||
|
# Validate 'password'
|
||||||
|
if not isinstance(data['password'], str) or not data['password'].strip():
|
||||||
|
return jsonify({"message": "Invalid password"}), 400
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
def validate_event_post_request(f):
|
def validate_event_post_request(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
@ -34,3 +91,17 @@ def validate_event_post_request(f):
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
def authenticate_user(f):
|
||||||
|
@wraps(f)
|
||||||
|
@jwt_required(locations=["cookies"]) # Specify to look for the token in cookies
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
# Get user identity from JWT
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
if user_id:
|
||||||
|
request.user_id = user_id
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Invalid session token"}), 401
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
46
migrations/versions/9d6b0ea04d2c_.py
Normal file
46
migrations/versions/9d6b0ea04d2c_.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 9d6b0ea04d2c
|
||||||
|
Revises: 5569d39a87cf
|
||||||
|
Create Date: 2024-01-07 11:34:58.903280
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '9d6b0ea04d2c'
|
||||||
|
down_revision = '5569d39a87cf'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('name', sa.String(length=100), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('email', sa.String(length=120), nullable=False))
|
||||||
|
batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True))
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.String(length=36),
|
||||||
|
existing_nullable=False)
|
||||||
|
batch_op.create_unique_constraint('uq_user_email', ['email']) # Added constraint name here
|
||||||
|
batch_op.drop_column('username')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('username', sa.VARCHAR(length=80), nullable=False))
|
||||||
|
batch_op.drop_constraint('uq_user_email', type_='unique') # Updated constraint name here
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.String(length=36),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=False)
|
||||||
|
batch_op.drop_column('location')
|
||||||
|
batch_op.drop_column('email')
|
||||||
|
batch_op.drop_column('name')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
36
migrations/versions/b962126d3578_.py
Normal file
36
migrations/versions/b962126d3578_.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: b962126d3578
|
||||||
|
Revises: 9d6b0ea04d2c
|
||||||
|
Create Date: 2024-01-07 11:41:03.752411
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b962126d3578'
|
||||||
|
down_revision = '9d6b0ea04d2c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('location',
|
||||||
|
existing_type=sa.VARCHAR(length=100),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('location',
|
||||||
|
existing_type=sa.VARCHAR(length=100),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
25
models.py
25
models.py
|
@ -1,6 +1,9 @@
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_bcrypt import Bcrypt
|
||||||
|
import uuid
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
bcrypt = Bcrypt()
|
||||||
|
|
||||||
class Event(db.Model):
|
class Event(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -20,3 +23,25 @@ class Event(db.Model):
|
||||||
'duedate': self.duedate.isoformat() if self.duedate else None,
|
'duedate': self.duedate.isoformat() if self.duedate else None,
|
||||||
'created_at': self.created_at.isoformat()
|
'created_at': self.created_at.isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||||
|
name = db.Column(db.String(100))
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(128))
|
||||||
|
location = db.Column(db.String(100), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return bcrypt.check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'email': self.email,
|
||||||
|
'location': self.location
|
||||||
|
}
|
0
routes/__init__.py
Normal file
0
routes/__init__.py
Normal file
|
@ -1,11 +1,12 @@
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
from services import EventService
|
from services.EventService import EventService
|
||||||
from middlewares import validate_event_post_request
|
from middlewares import validate_event_post_request
|
||||||
|
|
||||||
api = Blueprint('api', __name__)
|
eventRoutes = Blueprint('eventRoutes', __name__)
|
||||||
|
|
||||||
# Create new event
|
# Create new event
|
||||||
@api.route('/events', methods=['POST'])
|
@eventRoutes.route('/', methods=['POST'])
|
||||||
|
@eventRoutes.route('', methods=['POST'])
|
||||||
@validate_event_post_request
|
@validate_event_post_request
|
||||||
def create_event():
|
def create_event():
|
||||||
try:
|
try:
|
||||||
|
@ -19,7 +20,8 @@ def create_event():
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# Get All Events
|
# Get All Events
|
||||||
@api.route('/events', methods=['GET'])
|
@eventRoutes.route('/', methods=['GET'])
|
||||||
|
@eventRoutes.route('', methods=['GET'])
|
||||||
def get_events():
|
def get_events():
|
||||||
try:
|
try:
|
||||||
return {"events": EventService.get_all_events()}, 200
|
return {"events": EventService.get_all_events()}, 200
|
||||||
|
@ -27,7 +29,7 @@ def get_events():
|
||||||
return {"error": str(e)}, 500
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
# Get Event by ID
|
# Get Event by ID
|
||||||
@api.route('/events/<int:event_id>', methods=['GET'])
|
@eventRoutes.route('/<int:event_id>', methods=['GET'])
|
||||||
def get_event(event_id):
|
def get_event(event_id):
|
||||||
try:
|
try:
|
||||||
return {"event": EventService.get_event_by_id(event_id)}, 200
|
return {"event": EventService.get_event_by_id(event_id)}, 200
|
||||||
|
@ -35,7 +37,7 @@ def get_event(event_id):
|
||||||
return {"error": str(e)}, 500
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
# Update Event
|
# Update Event
|
||||||
@api.route('/events/<int:event_id>', methods=['PUT'])
|
@eventRoutes.route('/<int:event_id>', methods=['PUT'])
|
||||||
@validate_event_post_request
|
@validate_event_post_request
|
||||||
def update_event(event_id):
|
def update_event(event_id):
|
||||||
try:
|
try:
|
||||||
|
@ -49,7 +51,7 @@ def update_event(event_id):
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# DELETE Event
|
# DELETE Event
|
||||||
@api.route('/events/<int:event_id>', methods=['DELETE'])
|
@eventRoutes.route('/<int:event_id>', methods=['DELETE'])
|
||||||
def delete_event(event_id):
|
def delete_event(event_id):
|
||||||
try:
|
try:
|
||||||
deleted_event = EventService.delete_event(event_id)
|
deleted_event = EventService.delete_event(event_id)
|
46
routes/userRoutes.py
Normal file
46
routes/userRoutes.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from services.UserService import UserService
|
||||||
|
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity
|
||||||
|
from middlewares import validate_user_post_request, validate_user_login_request, authenticate_user
|
||||||
|
|
||||||
|
userRoutes = Blueprint('userRoutes', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@userRoutes.route('/', methods=['GET'])
|
||||||
|
@userRoutes.route('', methods=['GET'])
|
||||||
|
@authenticate_user
|
||||||
|
def allUsers():
|
||||||
|
users = UserService.get_all_users()
|
||||||
|
return jsonify(users), 200
|
||||||
|
|
||||||
|
@userRoutes.route('/', methods=['POST'])
|
||||||
|
@userRoutes.route('', methods=['POST'])
|
||||||
|
@validate_user_post_request
|
||||||
|
def createNewUser():
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
if UserService.get_user_by_email(data['email']):
|
||||||
|
return jsonify({'error': 'User already exists'}), 400
|
||||||
|
new_user = UserService.create_user(data)
|
||||||
|
if new_user:
|
||||||
|
return jsonify(new_user), 201
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Failed to create user'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@userRoutes.route('/login', methods=['POST'])
|
||||||
|
@validate_user_login_request
|
||||||
|
def loginUser():
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
user = UserService.verify_user(data)
|
||||||
|
if user:
|
||||||
|
sessionToken = create_access_token(identity=user.id)
|
||||||
|
response = jsonify(user.to_dict())
|
||||||
|
response.set_cookie('access_token_cookie', sessionToken, httponly=True, path='/')
|
||||||
|
return response, 200
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Invalid credentials'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
|
@ -22,6 +22,7 @@ class EventService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_event_by_id(event_id):
|
def get_event_by_id(event_id):
|
||||||
return Event.query.filter_by(id=event_id, deleted=False).first().to_dict()
|
return Event.query.filter_by(id=event_id, deleted=False).first().to_dict()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_event(event_id, data):
|
def update_event(event_id, data):
|
||||||
event = Event.query.get(event_id)
|
event = Event.query.get(event_id)
|
33
services/UserService.py
Normal file
33
services/UserService.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from flask_bcrypt import Bcrypt
|
||||||
|
from models import db, User
|
||||||
|
|
||||||
|
bcrypt = Bcrypt()
|
||||||
|
|
||||||
|
class UserService:
|
||||||
|
@staticmethod
|
||||||
|
def create_user(data):
|
||||||
|
new_user = User(
|
||||||
|
name=data['username'],
|
||||||
|
email=data['email'],
|
||||||
|
location=data['location'],
|
||||||
|
password_hash=bcrypt.generate_password_hash(data['password']).decode('utf-8')
|
||||||
|
)
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_users():
|
||||||
|
users = User.query.all()
|
||||||
|
return [user.to_dict() for user in users]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_by_email(email):
|
||||||
|
return User.query.filter_by(email=email).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def verify_user(data):
|
||||||
|
user = UserService.get_user_by_email(data['email'])
|
||||||
|
if user and bcrypt.check_password_hash(user.password_hash, data['password']):
|
||||||
|
return user
|
||||||
|
return None
|
Loading…
Reference in a new issue