From e35155a4ddd59bc52a657ed0c365753abb7416de Mon Sep 17 00:00:00 2001
From: Phil Owen <19691521+PhillipsOwen@users.noreply.github.com>
Date: Tue, 26 Nov 2024 08:27:19 -0500
Subject: [PATCH] adding user add/update/verify functionality
---
src/common/pg_impl.py | 77 ++++++++++++++++
src/common/pg_utils_multi.py | 4 +-
src/server.py | 166 +++++++++++++++++++++++++++++++++--
3 files changed, 239 insertions(+), 8 deletions(-)
diff --git a/src/common/pg_impl.py b/src/common/pg_impl.py
index 31e2792..6d336f0 100644
--- a/src/common/pg_impl.py
+++ b/src/common/pg_impl.py
@@ -601,3 +601,80 @@ def get_instance_names(self, name: str, project_code: str = None) -> EnumType:
# Return Pandas dataframe
return ret_val
+
+ def verify_user(self, email: str, password_hash: str) -> dict:
+ """
+ verifies the user has an account and the password is correct.
+
+ if the verification is successful, return a JSON object with pass/fail and user account data
+
+ :param email:
+ :param password_hash:
+ :return:
+ """
+ # init the return value:
+ ret_val = None
+
+ # prep the email param for the SP
+ if email is None:
+ email = 'null'
+ else:
+ email = f"'{email}'"
+
+ # prep the password param for the SP
+ if password_hash is None:
+ password_hash = 'null'
+ else:
+ password_hash = f"'{password_hash}'"
+
+ # build the query. this will also return the users profile
+ sql = f"SELECT verify_user(_email := {email}, _password_hash := {password_hash});"
+
+ # get the info
+ ret_val = self.exec_sql('apsviz', sql)
+
+ # return the result of the inquiry
+ return ret_val
+
+ def add_user(self, **kwargs) -> dict:
+ """
+ Adds the user and profile and returns a pass/fail dict
+
+ if the call is successful, returns a pass/fail dict
+
+ :param:
+ :return:
+ """
+ # init the return value:
+ ret_val = None
+
+ # build the query
+ sql = f"SELECT public.add_user(_email:={kwargs['email']}, _password_hash:={kwargs['password_hash']}, _role_id:={kwargs['role_id']}, _details:={kwargs['details']});"
+
+ # get the info
+ ret_val = self.exec_sql('apsviz', sql)
+
+ # Return Pandas dataframe
+ return ret_val
+
+ def update_user(self, **kwargs) -> dict:
+ """
+ Updates the user profile and returns a pass/fail dict
+
+ if the call is successful, returns a pass/fail dict
+
+ :param :
+
+ :return:
+ """
+ # init the return value:
+ ret_val = None
+
+ # create the SQL query
+ sql = f"SELECT public.update_user({kwargs['email']}, _password_hash:={kwargs['password_hash']}, _role_id:={kwargs['role_id']}, _details:={kwargs['details']});"
+
+ # get the info
+ ret_val = self.exec_sql('apsviz', sql)
+
+ # Return Pandas dataframe
+ return ret_val
diff --git a/src/common/pg_utils_multi.py b/src/common/pg_utils_multi.py
index 5f5bd5a..efc0092 100644
--- a/src/common/pg_utils_multi.py
+++ b/src/common/pg_utils_multi.py
@@ -251,7 +251,7 @@ def exec_sql(self, db_name: str, sql_stmt: str):
# insure we have a valid DB connection
success = self.get_db_connection(db_info)
- # did we get a connection
+ # if we get a connection
if success:
# init the cursor
cursor = None
@@ -274,7 +274,7 @@ def exec_sql(self, db_name: str, sql_stmt: str):
# specify a return code on an empty result
ret_val = -1
else:
- # get the one and only record of json
+ # get the result payload
ret_val = ret_val[0]
except Exception:
diff --git a/src/server.py b/src/server.py
index 3d250de..20abdf7 100644
--- a/src/server.py
+++ b/src/server.py
@@ -69,7 +69,7 @@ async def get_ui_data(grid_type: Union[str, None] = Query(default=None), event_t
limit: Union[int, None] = Query(default=7), use_new_wb: Union[bool, None] = Query(default=False),
use_v3_sp: Union[bool, None] = Query(default=False)) -> json:
"""
- Gets the json formatted map UI catalog data.
+ Gets the JSON formatted map UI catalog data.
Note: Leave filtering params empty if not desired.
grid_type: Filter by the name of the ECFLOW grid
event_type: Filter by the event type
@@ -94,7 +94,8 @@ async def get_ui_data(grid_type: Union[str, None] = Query(default=None), event_t
try:
logger.debug('Params - grid_type: %s, event_type: %s, instance_name: %s, met_class: %s, storm_name: %s, cycle: %s, advisory_number: %s, '
'run_date: %s, end_date: %s, project_code %s, product_type: %s, limit: %s, ensemble_name: %s', grid_type, event_type,
- instance_name, met_class, storm_name, cycle, advisory_number, run_date, end_date, project_code, product_type, limit, ensemble_name)
+ instance_name, met_class, storm_name, cycle, advisory_number, run_date, end_date, project_code, product_type, limit,
+ ensemble_name)
# init the kwargs variable
kwargs: dict = {}
@@ -264,7 +265,7 @@ async def get_ui_data_secure(run_id: Union[str, None] = Query(default=None), gri
product_type: Union[str, None] = Query(default=None), limit: Union[int, None] = Query(default=7),
use_new_wb: Union[bool, None] = Query(default=False), use_v3_sp: Union[bool, None] = Query(default=False), ) -> json:
"""
- Gets the json formatted map UI catalog data.
+ Gets the JSON formatted map UI catalog data.
4460-2024020500-gfsforecast
Note: Leave filtering params empty if not desired.
run_id: Filter by the run ID
@@ -353,7 +354,7 @@ async def get_ui_data_file(file_name: Union[str, None] = Query(default='apsviz.j
product_type: Union[str, None] = Query(default=None), limit: Union[int, None] = Query(default=7),
use_new_wb: Union[bool, None] = Query(default=False), use_v3_sp: Union[bool, None] = Query(default=False), ) -> json:
"""
- Returns the json formatted map UI catalog data in a file specified.
+ Returns the JSON formatted map UI catalog data in a file specified.
Note: Leave filtering params empty if not desired.
file_name: The name of the output file (default is apsviz.json)
grid_type: Filter by the name of the grid
@@ -644,7 +645,7 @@ async def get_station_data_file(file_name: Union[str, None] = Query(default='sta
async def get_catalog_member_records(run_id: Union[str, None] = Query(default=None), project_code: Union[str, None] = Query(default=None),
filter_event_type: Union[str, None] = Query(default=None), limit: Union[int, None] = Query(default=4)) -> json:
"""
- Gets the json formatted catalog member data.
+ Gets the JSON formatted catalog member data.
Note: Leave filtering params empty if not desired.
run_id: Filter by the name of the ECFLOW grid. Leaving this empty will result in getting the latest records.
project_code: Filter by the project code.
@@ -713,7 +714,7 @@ async def get_pulldown_data(grid_type: Union[str, None] = Query(default=None), e
end_date: Union[str, None] = Query(default=None), project_code: Union[str, None] = Query(default=None),
product_type: Union[str, None] = Query(default=None), psc_output: bool = False) -> json:
"""
- Gets the json formatted UI pulldown data.
+ Gets the JSON formatted UI pulldown data.
Note: Leave filtering params empty if not desired.
grid_type: Filter by the name of the ECFLOW grid
event_type: Filter by the event type
@@ -778,3 +779,156 @@ async def get_pulldown_data(grid_type: Union[str, None] = Query(default=None), e
# return to the caller
return JSONResponse(content=ret_val, status_code=status_code, media_type="application/json")
+
+
+@APP.get('/verify_user', status_code=200, response_model=None)
+async def verify_user(email: Union[str, None] = Query(default=None), password_hash: Union[str, None] = Query(default=None)):
+ """
+ Verifies that the user exists and returns their profile if they do.
+
+
The user's email address
+
The user's password (hashed)
+ """
+ # pylint: disable=locally-disabled, unused-argument
+
+ # init the returned data and HTML status code
+ ret_val: dict = {}
+ status_code: int = 200
+
+ try:
+ # try to make the call for records
+ ret_val: dict = db_info.verify_user(email, password_hash)
+
+ # check the return
+ if ret_val['success']:
+ # create the JWT payload
+ payload = {'bearer_name': os.environ.get("BEARER_NAME"), 'bearer_secret': os.environ.get("BEARER_SECRET")}
+
+ # create an access token
+ token = security.sign_jwt(payload)
+
+ # create a new dict element with the JWT token
+ ret_val['token'] = token['access_token']
+ # the verification was not successful
+ else:
+ ret_val = {'Error': "Could not verify the user's credentials."}
+
+ # set the status to a server error
+ status_code = 404
+
+ except Exception:
+ # return a failure message
+ ret_val = {'Error': 'Exception detected trying to verify the user.'}
+
+ # log the exception
+ logger.exception(ret_val)
+
+ # set the status to a server error
+ status_code = 500
+
+ # return to the caller
+ return JSONResponse(content=ret_val, status_code=status_code, media_type="application/json")
+
+
+@APP.get('/update_user', status_code=200, response_model=None)
+async def update_user(email: Union[str, None] = Query(default=None), password_hash: Union[str, None] = Query(default=None),
+ role_id: Union[str, None] = Query(default=None), details: Union[str, None] = Query(default=None)):
+ """
+ update_user the user profile.
+
The user's email address
+
The user's password (hashed)
+
The user's role
+
The user's details
+ """
+ # pylint: disable=locally-disabled, unused-argument
+
+ # init the returned data and HTML status code
+ ret_val: dict = {}
+ status_code: int = 200
+
+ try:
+ # init the kwargs variable
+ kwargs: dict = {}
+
+ # create the param list
+ params: list = ['email', 'password_hash', 'role_id', 'details']
+
+ # loop through the SP params passed in
+ for param in params:
+ # add this parm to the list
+ kwargs.update({param: 'null' if not locals()[param] else f"'{locals()[param]}'"})
+
+ # try to make the call for records
+ ret_val: dict = db_info.update_user(**kwargs)
+
+ # check the return
+ if not ret_val['success']:
+ ret_val = {'Error': 'Database error updating the users information.'}
+
+ # set the status to a server error
+ status_code = 500
+
+ except Exception:
+ # return a failure message
+ ret_val = {'Error': 'Exception detected trying to update the user profile.'}
+
+ # log the exception
+ logger.exception(ret_val)
+
+ # set the status to a server error
+ status_code = 500
+
+ # return to the caller
+ return JSONResponse(content=ret_val, status_code=status_code, media_type="application/json")
+
+
+@APP.get('/add_user', status_code=200, response_model=None)
+async def add_user(email: Union[str, None] = Query(default=None), password_hash: Union[str, None] = Query(default=None),
+ role_id: Union[str, None] = Query(default=None), details: Union[str, None] = Query(default=None)):
+ """
+ Adds the user and their profile.
+
The user's email address
+
The user's password (hashed)
+
The user's role
+
The user's details
+
+ """
+ # pylint: disable=locally-disabled, unused-argument
+
+ # init the returned data and HTML status code
+ ret_val: dict = {}
+ status_code: int = 200
+
+ try: # init the kwargs variable
+ kwargs: dict = {}
+
+ # create the param list
+ params: list = ['email', 'password_hash', 'role_id', 'details']
+
+ # loop through the SP params passed in
+ for param in params:
+ # add this parm to the list
+ kwargs.update({param: 'null' if not locals()[param] else f"'{locals()[param]}'"})
+
+ # try to make the call for records
+ ret_val: dict = db_info.add_user(**kwargs)
+
+ # check the return
+ if not ret_val['success']:
+ ret_val = {'Error': 'Database error adding the user.'}
+
+ # set the status to a server error
+ status_code = 500
+
+ except Exception:
+ # return a failure message
+ ret_val = {'Error': 'Exception detected trying to add the user.'}
+
+ # log the exception
+ logger.exception(ret_val)
+
+ # set the status to a server error
+ status_code = 500
+
+ # return to the caller
+ return JSONResponse(content=ret_val, status_code=status_code, media_type="application/json")