Added user authentication capabilities to hub
This commit is contained in:
parent
23ce8665a2
commit
4693edd5e8
@ -46,12 +46,12 @@ from exports.db_factory import DBFactory
|
||||
from pathlib import Path
|
||||
|
||||
dotenv_path = (Path(__file__).parent / '.env').resolve()
|
||||
factory = DBFactory(db_name='hub_db', app_env='PROD', dotenv_path=dotenv_path, city=None)
|
||||
factory = DBFactory(db_name='hub_db', app_env='PROD', dotenv_path=dotenv_path)
|
||||
```
|
||||
|
||||
|
||||
## Create Database Tables ##
|
||||
Use the *DBSetUp* class in the persistence package to create the required database tables as described below
|
||||
Use the *DBSetup* class in the persistence package to create the required database tables as described below
|
||||
```python
|
||||
from persistence import DBSetup
|
||||
from pathlib import Path
|
||||
@ -59,3 +59,8 @@ from pathlib import Path
|
||||
dotenv_path = (Path(__file__).parent / '.env').resolve()
|
||||
DBSetup(db_name='hub_db', app_env='PROD', dotenv_path=dotenv_path)
|
||||
```
|
||||
The *DBSetUp* class also creates a default admin user with default credentials that can be changed.
|
||||
with the import UserFactory class. The admin user (name, email, password and role) is logged into the console after it is created by the
|
||||
*constructor of DBSetup*. Use can also manage users (create, read, update and delete) with user import and export factories.
|
||||
|
||||
**NB: Make sure to change the default admin user credentials**
|
||||
|
31
exports/user_factory.py
Normal file
31
exports/user_factory.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""
|
||||
User performs user related crud operations
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2022 Concordia CERC group
|
||||
Project CoderPeter Yefi peteryefi@gmail.com
|
||||
"""
|
||||
from persistence import UserRepo
|
||||
|
||||
|
||||
class UserFactory:
|
||||
"""
|
||||
UserFactory class
|
||||
"""
|
||||
|
||||
def __init__(self, db_name, app_env, dotenv_path):
|
||||
self._user_repo = UserRepo(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
|
||||
|
||||
def login_user(self, email: str, password: str):
|
||||
"""
|
||||
Retrieve a single city from postgres
|
||||
:param email: the email of the user
|
||||
:param password: the password of the user
|
||||
"""
|
||||
return self._user_repo.get_user_by_email_and_password(email, password)
|
||||
|
||||
def get_user_by_email(self, email):
|
||||
"""
|
||||
Retrieve a single user
|
||||
:param email: the email of the user to get
|
||||
"""
|
||||
return self._user_repo.get_by_email(email)
|
43
helpers/auth.py
Normal file
43
helpers/auth.py
Normal file
@ -0,0 +1,43 @@
|
||||
import bcrypt
|
||||
import re
|
||||
|
||||
|
||||
class Auth(object):
|
||||
|
||||
@staticmethod
|
||||
def validate_password(password: str) -> bool:
|
||||
"""
|
||||
Validates a password
|
||||
:param password: the password to validate
|
||||
:return:
|
||||
"""
|
||||
pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!#%*?&]{6,20}$"
|
||||
pattern = re.compile(pattern)
|
||||
if not re.search(pattern, password):
|
||||
raise ValueError("Password must be between 6 to 20 characters and must have at least a number, an uppercase "
|
||||
"letter, a lowercase letter, and a special character")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
"""
|
||||
Hashes a password
|
||||
:param password: the password to be hashed
|
||||
:return:
|
||||
"""
|
||||
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(14)).decode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
def check_password(password: str, hashed_password) -> bool:
|
||||
"""
|
||||
Hashes a password
|
||||
:param password: the password to be checked
|
||||
:param hashed_password: the hashed password
|
||||
:return:
|
||||
"""
|
||||
return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,122 +0,0 @@
|
||||
from persistence.models import Building
|
||||
from city_model_structure.building_demand.surface import Surface
|
||||
from typing import Dict, List, Union
|
||||
import numpy as np
|
||||
|
||||
|
||||
class CityUtil(object):
|
||||
# holds a single instance of this class
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __new__(cls):
|
||||
"""
|
||||
Implemented for a singleton pattern
|
||||
"""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(CityUtil, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def _class_object_to_dict(self, class_obj) -> Union[Dict, None]:
|
||||
"""
|
||||
converts a class object to a dictionary
|
||||
:param class_obj: the class object
|
||||
:return:
|
||||
"""
|
||||
if class_obj is None:
|
||||
return None
|
||||
if type(class_obj) is not dict:
|
||||
return vars(class_obj)
|
||||
return class_obj
|
||||
|
||||
def _ndarray_to_list(self, key, building_dict) -> Union[Dict, None]:
|
||||
"""
|
||||
Converts a numpy array to dictionary
|
||||
:param key: the key to the dictionary value
|
||||
:param building_dict: the dictionary
|
||||
:return:
|
||||
"""
|
||||
if dict is not None:
|
||||
if type(building_dict[key]) is np.ndarray:
|
||||
return building_dict[key].tolist()
|
||||
else:
|
||||
return building_dict[key]
|
||||
return None
|
||||
|
||||
def object_list_to_dict_list(self, object_list):
|
||||
"""
|
||||
converts a list of objects to a list of dictionaries
|
||||
:param object_list: a list of objects
|
||||
:return:
|
||||
"""
|
||||
if object_list is None:
|
||||
return []
|
||||
return [self._class_object_to_dict(obj) for obj in object_list]
|
||||
|
||||
def _serialize_points(self, points) -> List:
|
||||
"""
|
||||
Deserializes arry of Point objects to array of dictionarier
|
||||
:param points: a Point object
|
||||
:return a list of points []:
|
||||
"""
|
||||
if points is None:
|
||||
return None
|
||||
serialized_points = []
|
||||
for i in range(len(points)):
|
||||
point = self._class_object_to_dict(points[i])
|
||||
point['_coordinates'] = self._ndarray_to_list('_coordinates', point)
|
||||
serialized_points.append(point)
|
||||
return serialized_points
|
||||
|
||||
def extract_building_data(self, building: Building) -> Dict:
|
||||
"""
|
||||
Extracts various values from a building
|
||||
:param building: the building object
|
||||
:return: a dictionary {} of building attributes
|
||||
"""
|
||||
dict_building = {}
|
||||
for key, value in vars(building).items():
|
||||
if key in ['_name', '_year_of_construction', '_function', '_floor_area']:
|
||||
continue
|
||||
if type(value) is list:
|
||||
if len(value) == 0:
|
||||
dict_building[key] = []
|
||||
elif len(value) > 0:
|
||||
if type(value[0]) is Surface:
|
||||
try:
|
||||
surfaces = []
|
||||
for surface in value:
|
||||
surface_dict = vars(surface)
|
||||
perimeter_polygon = self._class_object_to_dict(surface_dict['_perimeter_polygon'])
|
||||
solid_polygon = self._class_object_to_dict(surface_dict['_solid_polygon'])
|
||||
holes_polygon = self._class_object_to_dict(surface_dict['_holes_polygons'])
|
||||
|
||||
if perimeter_polygon is not None:
|
||||
perimeter_polygon['_coordinates'] = self._ndarray_to_list('_coordinates', perimeter_polygon)
|
||||
perimeter_polygon['_points'] = self._serialize_points(perimeter_polygon['_points'])
|
||||
surface_dict['_perimeter_polygon'] = perimeter_polygon
|
||||
|
||||
if holes_polygon is not None:
|
||||
holes_polygon['_coordinates'] = self._ndarray_to_list('_coordinates', holes_polygon)
|
||||
holes_polygon['_points'] = self._serialize_points(holes_polygon['_points'])
|
||||
surface_dict['_holes_polygons'] = holes_polygon
|
||||
|
||||
if solid_polygon is not None:
|
||||
solid_polygon['_coordinates'] = self._ndarray_to_list('_coordinates', solid_polygon)
|
||||
solid_polygon['_points'] = self._serialize_points(solid_polygon['_points'])
|
||||
surface_dict['_solid_polygon'] = solid_polygon
|
||||
|
||||
surfaces.append(surface_dict)
|
||||
dict_building[key] = surfaces
|
||||
except KeyError as err:
|
||||
print(f'Dictionary key error: {err}')
|
||||
elif value is None:
|
||||
dict_building[key] = None
|
||||
elif type(value) in [str, int, dict, np.float64]:
|
||||
dict_building[key] = value
|
||||
elif type(value) is np.ndarray:
|
||||
dict_building[key] = value.tolist()
|
||||
|
||||
return dict_building
|
45
imports/user_factory.py
Normal file
45
imports/user_factory.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""
|
||||
User performs user-related crud operations
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2022 Concordia CERC group
|
||||
Project CoderPeter Yefi peteryefi@gmail.com
|
||||
"""
|
||||
from persistence import UserRepo
|
||||
from persistence import UserRoles
|
||||
|
||||
|
||||
class UserFactory:
|
||||
"""
|
||||
UserFactory class
|
||||
"""
|
||||
|
||||
def __init__(self, db_name, app_env, dotenv_path):
|
||||
self._user_repo = UserRepo(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
|
||||
|
||||
def create_user(self, name: str, email: str, password: str, role: UserRoles):
|
||||
"""
|
||||
Creates a new user
|
||||
:param name: the name of the user
|
||||
:param email: the email of the user
|
||||
:param password: the password of the user
|
||||
:param role: the role of the user
|
||||
"""
|
||||
return self._user_repo.insert(name, email, password, role)
|
||||
|
||||
def update_user(self, user_id: int, name: str, email: str, password: str, role: UserRoles):
|
||||
"""
|
||||
Creates a new user
|
||||
:param user_id: the id of the user
|
||||
:param name: the name of the user
|
||||
:param email: the email of the user
|
||||
:param password: the password of the user
|
||||
:param role: the role of the user
|
||||
"""
|
||||
return self._user_repo.update(user_id, name, email, password, role)
|
||||
|
||||
def delete_user(self, user_id):
|
||||
"""
|
||||
Retrieve a single user
|
||||
:param user_id: the id of the user to delete
|
||||
"""
|
||||
return self._user_repo.delete_user(user_id)
|
@ -1,5 +1,6 @@
|
||||
from .base_repo import BaseRepo
|
||||
from .repositories.city_repo import CityRepo
|
||||
from .repositories.building_repo import BuildingRepo
|
||||
from .repositories.heat_pump_simulation_repo import HeatPumpSimulationRepo
|
||||
from .db_setup import DBSetup
|
||||
from .repositories.user_repo import UserRepo
|
||||
from .models.user import UserRoles
|
||||
|
@ -1,11 +1,31 @@
|
||||
from persistence.models import City
|
||||
from persistence import BaseRepo
|
||||
from persistence.models import HeatPumpSimulation
|
||||
from persistence.models import User
|
||||
from persistence.repositories import UserRepo
|
||||
from persistence.models import UserRoles
|
||||
|
||||
|
||||
class DBSetup:
|
||||
|
||||
def __init__(self, db_name, app_env, dotenv_path):
|
||||
"""
|
||||
Creates database tables and a default admin user
|
||||
:param db_name:
|
||||
:param app_env:
|
||||
:param dotenv_path:
|
||||
"""
|
||||
repo = BaseRepo(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
|
||||
City.__table__.create(bind=repo.engine, checkfirst=True)
|
||||
HeatPumpSimulation.__table__.create(bind=repo.engine, checkfirst=True)
|
||||
User.__table__.create(bind=repo.engine, checkfirst=True)
|
||||
self._user_repo = UserRepo(db_name=db_name, app_env=app_env, dotenv_path=dotenv_path)
|
||||
self._create_admin_user(self._user_repo)
|
||||
|
||||
def _create_admin_user(self, user_repo):
|
||||
email = 'admin@hub.com'git
|
||||
password = 'HubAdmin#!98'
|
||||
print('Creating default admin user...')
|
||||
user_repo.insert('Administrator', email, password, UserRoles.Admin)
|
||||
print(f'Created Admin user with email: {email}, password: {password} and role: {UserRoles.Admin}')
|
||||
print('Remember to change the admin default password and email address with the UserFactory')
|
||||
|
@ -1,5 +1,5 @@
|
||||
from .building import Building
|
||||
from .city import City
|
||||
from .heat_pump_simulation import HeatPumpSimulation
|
||||
from .heat_pump_simulation import SimulationTypes
|
||||
from .heat_pump_simulation import HeatPumpTypes
|
||||
from .user import User, UserRoles
|
||||
|
@ -1,31 +0,0 @@
|
||||
"""
|
||||
Model representation of a building
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2022 Concordia CERC group
|
||||
Project Coder Peter Yefi peteryefi@gmail.com
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Sequence, Float, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from persistence.db_config import Base
|
||||
|
||||
|
||||
class Building(Base):
|
||||
"""A model representation of a building
|
||||
|
||||
Attributes:
|
||||
city_id A reference to the city which has this building.
|
||||
name The name of the building.
|
||||
construction_year The year of construction of the building.
|
||||
function The function (e.g. residential) of the building.
|
||||
floor_area The computed area of the floor of the building.
|
||||
data A JSON object which contain other data (like roof, walls, zones, etc) of the building
|
||||
"""
|
||||
__tablename__ = "building"
|
||||
id = Column(Integer, Sequence('building_id_seq'), primary_key=True)
|
||||
city_id = Column(Integer, ForeignKey('city.id'), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
construction_year = Column(Integer, nullable=False)
|
||||
function = Column(String, nullable=False)
|
||||
floor_area = Column(Float, nullable=False)
|
||||
data = Column(JSONB, nullable=False)
|
45
persistence/models/user.py
Normal file
45
persistence/models/user.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""
|
||||
Model representation of a User
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2022 Concordia CERC group
|
||||
Project Coder Peter Yefi peteryefi@gmail.com
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy import DateTime, Enum
|
||||
from persistence.db_config import Base
|
||||
import datetime
|
||||
from sqlalchemy.orm import validates
|
||||
import re
|
||||
import enum
|
||||
|
||||
|
||||
class UserRoles(enum.Enum):
|
||||
Admin = 'ADMIN'
|
||||
HubReader = 'HUB_READER'
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""A model representation of a city
|
||||
"""
|
||||
__tablename__ = "user"
|
||||
id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
email = Column(String, nullable=False, unique=True)
|
||||
password = Column(String, nullable=False)
|
||||
role = Column(Enum(UserRoles), nullable=False, default=UserRoles.HubReader)
|
||||
created = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
@validates("email")
|
||||
def validate_email(self, key, address):
|
||||
pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
|
||||
if not re.match(pattern, address):
|
||||
raise ValueError("failed simple email validation")
|
||||
return address
|
||||
|
||||
def __init__(self, name, email, password, role):
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.role = role
|
@ -1 +1 @@
|
||||
|
||||
from .user_repo import UserRepo
|
||||
|
@ -1,92 +0,0 @@
|
||||
"""
|
||||
Building repository with database CRUD operations
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2022 Concordia CERC group
|
||||
Project Coder Peter Yefi peteryefi@gmail.com
|
||||
"""
|
||||
|
||||
from city_model_structure.building import Building as CityBuilding
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from persistence.models import Building
|
||||
from persistence import BaseRepo
|
||||
from helpers.city_util import CityUtil
|
||||
from sqlalchemy import select
|
||||
|
||||
|
||||
class BuildingRepo(BaseRepo):
|
||||
|
||||
def __init__(self, db_name):
|
||||
super().__init__(db_name)
|
||||
self._city_util = CityUtil()
|
||||
|
||||
def insert(self, building: CityBuilding, city_id: int) -> Building:
|
||||
"""
|
||||
Inserts a new building into the database
|
||||
:param building: the building to insert
|
||||
:param city_id: the city the building belongs to
|
||||
:return:
|
||||
"""
|
||||
|
||||
model_building = Building()
|
||||
model_building.name = building.name
|
||||
model_building.function = building.function
|
||||
model_building.construction_year = building.year_of_construction
|
||||
model_building.floor_area = building.floor_area
|
||||
model_building.city_id = city_id
|
||||
model_building.data = self._city_util.extract_building_data(building)
|
||||
try:
|
||||
self.session.add(model_building)
|
||||
self.session.flush()
|
||||
self.session.commit()
|
||||
return model_building
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while adding building: {err}')
|
||||
|
||||
def get_by_id(self, building_id: int) -> Building:
|
||||
"""
|
||||
Fetch a building based on the id
|
||||
:param building_id: the building id
|
||||
:return: a Building
|
||||
"""
|
||||
try:
|
||||
return self.session.execute(select(Building).where(Building.id == building_id)).first()[0]
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while fetching building: {err}')
|
||||
|
||||
def get_by_name(self, building_name: str) -> [Building]:
|
||||
"""
|
||||
Fetch a building based on the name
|
||||
:param building_name: the name of the building
|
||||
:return: [Building] with the provided name
|
||||
"""
|
||||
try:
|
||||
result_set = self.session.execute(select(Building).where(Building.name == building_name))
|
||||
return [building[0] for building in result_set]
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while fetching buildings: {err}')
|
||||
|
||||
def get_by_construction_year(self, year: int):
|
||||
"""
|
||||
Fetch a building based on the year of construction
|
||||
:param year: the construction year of the building
|
||||
:return: [Building]
|
||||
"""
|
||||
try:
|
||||
result_set = self.session.execute(select(Building).where(Building.construction_year == year))
|
||||
return [building[0] for building in result_set]
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while fetching buildings: {err}')
|
||||
|
||||
def get_by_city(self, city_id: int):
|
||||
"""
|
||||
Get all the buildings in a city
|
||||
:param city_id: the city id
|
||||
:return: [Building]
|
||||
"""
|
||||
try:
|
||||
result_set = self.session.execute(select(Building).where(Building.city_id == city_id))
|
||||
return [building[0] for building in result_set]
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while fetching buildings: {err}')
|
||||
|
||||
|
101
persistence/repositories/user_repo.py
Normal file
101
persistence/repositories/user_repo.py
Normal file
@ -0,0 +1,101 @@
|
||||
"""
|
||||
City repository with database CRUD operations
|
||||
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
||||
Copyright © 2022 Concordia CERC group
|
||||
Project Coder Peter Yefi peteryefi@gmail.com
|
||||
"""
|
||||
|
||||
from persistence import BaseRepo
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import select
|
||||
from persistence.models import User
|
||||
from persistence.models import UserRoles
|
||||
from helpers.auth import Auth
|
||||
from typing import Union, Dict
|
||||
|
||||
|
||||
class UserRepo(BaseRepo):
|
||||
_instance = None
|
||||
|
||||
def __init__(self, db_name: str, dotenv_path: str, app_env: str):
|
||||
super().__init__(db_name, dotenv_path, app_env)
|
||||
|
||||
def __new__(cls, db_name, dotenv_path, app_env):
|
||||
"""
|
||||
Implemented for a singleton pattern
|
||||
"""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(UserRepo, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def insert(self, name: str, email: str, password: str, role: UserRoles) -> Union[User, Dict]:
|
||||
user = self.get_by_email(email)
|
||||
if user is None:
|
||||
try:
|
||||
if Auth.validate_password(password):
|
||||
user = User(name=name, email=email, password=Auth.hash_password(password), role=role)
|
||||
self.session.add(user)
|
||||
self.session.flush()
|
||||
self.session.commit()
|
||||
return user
|
||||
except SQLAlchemyError as err:
|
||||
print(f'An error occured while creating user: {err}')
|
||||
else:
|
||||
return {'message': f'user with {email} email already exists'}
|
||||
|
||||
def update(self, user_id: int, name: str, email: str, password: str, role: UserRoles):
|
||||
"""
|
||||
Updates a user
|
||||
:param user_id: the id of the user to be updated
|
||||
:param name: the name of the user
|
||||
:param email: the email of the user
|
||||
:param password: the password of the user
|
||||
:param role: the role of the user
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
if Auth.validate_password(password):
|
||||
self.session.query(User).filter(User.id == user_id) \
|
||||
.update({'name': name, 'email': email, 'password': Auth.hash_password(password), 'role': role})
|
||||
self.session.commit()
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while updating user: {err}')
|
||||
|
||||
def get_by_email(self, email: str) -> [User]:
|
||||
"""
|
||||
Fetch user based on the email address
|
||||
:param email: the email of the user
|
||||
:return: [User] with the provided email
|
||||
"""
|
||||
try:
|
||||
return self.session.execute(select(User).where(User.email == email)).first()
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while fetching user by email: {err}')
|
||||
|
||||
def delete_user(self, user_id: int):
|
||||
"""
|
||||
Deletes a user with the id
|
||||
:param user_id: the user id
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
self.session.query(User).filter(User.id == user_id).delete()
|
||||
self.session.commit()
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while fetching user: {err}')
|
||||
|
||||
def get_user_by_email_and_password(self, email: str, password: str) -> [User]:
|
||||
"""
|
||||
Fetch user based on the email and password
|
||||
:param email: the email of the user
|
||||
:param password: the password of the user
|
||||
:return: [User] with the provided email and password
|
||||
"""
|
||||
try:
|
||||
user = self.session.execute(select(User).where(User.email == email)).first()
|
||||
if user:
|
||||
if Auth.check_password(password, user[0].password):
|
||||
return user
|
||||
return {'message': 'user not found'}
|
||||
except SQLAlchemyError as err:
|
||||
print(f'Error while fetching user by email: {err}')
|
@ -18,3 +18,4 @@ PyYAML
|
||||
pyecore==0.12.2
|
||||
python-dotenv
|
||||
SQLAlchemy
|
||||
bcrypt==4.0.1
|
||||
|
Loading…
Reference in New Issue
Block a user