From affec1bf37b67b6100002033b45f675f238736ee Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Fri, 19 Mar 2021 16:34:38 +0000 Subject: [PATCH] Working on user module --- app/db_sql.py | 2 +- app/routers/user.py | 222 ++++++++++++++++++++++++++++++-------------- 2 files changed, 152 insertions(+), 72 deletions(-) diff --git a/app/db_sql.py b/app/db_sql.py index c7c3fc2..77c0004 100644 --- a/app/db_sql.py +++ b/app/db_sql.py @@ -108,7 +108,7 @@ def sql_insert(sql:str=None, data:dict=None, table_name:str=None, id_random_leng # ### BEGIN ### Core Help CRUD ### sql_update() ### -def sql_update(sql:str=None, data:dict=None, table_name:str=None, record_id:int=None, record_id_random:str=None, rm_id_random=None, id_random_length:int=8): +def sql_update(sql:str=None, data:dict=None, table_name:str=None, record_id:int=None, record_id_random:str=None, rm_id_random=None, id_random_length:None|int=8): log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) diff --git a/app/routers/user.py b/app/routers/user.py index 7b1f28c..f1564d7 100644 --- a/app/routers/user.py +++ b/app/routers/user.py @@ -11,6 +11,7 @@ from app.db_sql import * from .api_crud import delete_obj_template, get_obj_template, get_obj_li_template, patch_obj_template, post_obj_template +from ..models.common_field_schema import default_num_bytes from ..models.user_model import User_Base, User_New_Base, User_Out_Base from ..models.user_methods import load_user_obj from ..models.response_model import * @@ -77,6 +78,51 @@ async def post_user_new_obj( return mk_resp(data=False, status_message='The user account was not created. Something seems to have gone wrong on insert.') +@router.patch('/change_password/{user_id}', response_model=Resp_Body_Base) +async def change_user_obj_password( + user_id: Union[int,str], + password: Optional[str] = Query(None, min_length=6, max_length=50), + x_account_id: Optional[str] = Header(..., ), + return_obj: bool = False, + inc_contact: bool = False, + inc_organization: bool = False, + inc_person: bool = False, + by_alias: bool = True, + exclude_unset: bool = True, + ): + log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.debug(locals()) + + if password and len(password) >= 10: pass + else: + log.warning('The password given must be at least 10 characters. Generating a new random password.') + password = secrets.token_urlsafe(default_num_bytes) + + if user_id := redis_lookup_id_random(record_id_random=user_id, table_name='user'): pass + else: return mk_resp(data=False, status_code=404) # Not Found + + user_data = {} + #user_data['user_id'] = user_id + #user_data['username'] = username #???? + user_data['password'] = secure_hash_string(string=password) + + table_name = 'user' + user_rec_update_result = sql_update(data=user_data, table_name=table_name, record_id=user_id, id_random_length=None) + + if return_obj: + user_obj = load_user_obj( + user_id=user_id, + inc_contact=inc_contact, + inc_organization=inc_organization, + inc_person=inc_person + ).dict(by_alias=by_alias, exclude_unset=exclude_unset) + data = user_obj + else: + data = True + return mk_resp(data=data) + #return mk_resp(data=None, status_code=501) # Not Implemented + + @router.patch('/{obj_id}', response_model=Resp_Body_Base) async def patch_user_obj( obj_id: str = Query(..., min_length=1, max_length=22), @@ -104,72 +150,114 @@ async def patch_user_obj( return result -# This will look up a user based on the auth key given -# This can only be done once per key. It will be deleted if found -# A new one will need to be requested for a particular user each time -@router.get('/authenticate/auth_key/{auth_key}', response_model=Resp_Body_Base) -async def auth_key_get_user_obj( - auth_key: str = Query(..., min_length=11, max_length=22), +# ### BEGIN ### API User Routers ### user_authenticate() ### +# Authenticate a username and password OR by authorization key +# An authorization key can only be done once. It will be deleted if found. +# A new key will need to be requested for a particular user each time. +@router.get('/authenticate', response_model=Resp_Body_Base) +async def user_authenticate( + account_id: Optional[Union[int,str]] = None, + username: Optional[str] = Query(None, min_length=2, max_length=50), + password: Optional[str] = Query(None, min_length=6, max_length=50), + auth_key: Optional[str] = Query(None, min_length=11, max_length=22), x_account_id: str = Header(...), - by_alias: Optional[bool] = True, - exclude_unset: Optional[bool] = True, - exclude_none: Optional[bool] = True, + inc_contact: bool = False, + inc_organization: bool = False, + inc_person: bool = False, + by_alias: bool = True, + exclude_unset: bool = True, + exclude_none: bool = True, ): - log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) - if sql_select_result := sql_select(table_name='user', field_name='auth_key', field_value=auth_key): - log.debug(sql_select_result) + if account_id and username and password: + if account_id := redis_lookup_id_random(record_id_random=account_id, table_name='account'): pass + else: return mk_resp(data=False, status_code=404) # Not Found - resp_data = {} - resp_data['user_id_random'] = sql_select_result.get('id_random') - resp_data['username'] = sql_select_result.get('username') - resp_data['enable'] = sql_select_result.get('enable') - resp_data['enable_from'] = sql_select_result.get('enable_from') - resp_data['enable_to'] = sql_select_result.get('enable_to') - try: - user_obj = User_Base(**sql_select_result).dict(by_alias=by_alias, exclude_unset=exclude_unset, exclude_none=exclude_none) - log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL - log.debug(user_obj) - log.debug(user_obj.get('enable_from', None)) - except ValidationError as e: - log.error(e.json()) + user_data = {} + user_data['account_id'] = account_id + user_data['username'] = username + + sql_select(table_name='user', data=user_data) + + sql = f""" + SELECT `user`.id AS 'user_id', `user`.id_random AS 'user_id_random', `user`.password, `user`.enable, `user`.enable_from, `user`.enable_to + FROM `user` AS `user` + WHERE `user`.account_id = :account_id AND `user`.username = :username + LIMIT 1 + """ + + # This will return a list if selecting by account ID + if user_rec_result := sql_select(data=user_data, sql=sql): + user_id = user_rec_result.get('user_id', None) + + if password_hash := user_rec_result.get('password', None): + if verify_secure_hash_string(string=password, string_hash=password_hash): + log.info('The username was found, and the password matched.') + #return mk_resp(data=False, status_message='The username was found, and the password matched.') + else: + log.info('The username was found, but the password did not match.') + return mk_resp(data=False, status_message='The username was found, but the password did not match.') + else: + log.error('The password has was not found. This should not happen.') + return mk_resp(data=False, status_message='The password has was not found. This should not happen.') + + else: return mk_resp(data=None, status_code=404, status_message='The user account was not found') + elif auth_key: + if user_rec_result := sql_select(table_name='user', field_name='auth_key', field_value=auth_key): + update_user_data = {} + update_user_data['id'] = user_rec_result.get('id', None) + update_user_data['auth_key'] = None + + if user_rec_update_result := sql_update(table_name='user', data=update_user_data): + log.info('The user record was updated with a NULL auth_key') + else: + log.info('The user record was not updated with a NULL auth_key') + log.debug(user_rec_update_result) + + user_id = user_rec_result.get('id', None) # NOTE: This us looking for "id", not "user_id" + else: return mk_resp(data=None, status_code=404, status_message='A user account with that auth key was not found') + else: + return mk_resp(data=None, status_code=400, status_message='One more user account fields was missing or unexpected.') # Bad Request + log.debug(user_rec_result) + + if isinstance(user_rec_result, dict): current_utc_datetime = datetime.datetime.now(datetime.timezone.utc) - log.debug(user_obj.get('enable_from', None).astimezone(pytz.UTC)) - user_enable_from = user_obj.get('enable_from', None).astimezone(pytz.UTC) - log.debug(user_enable_from) - - log.debug(user_obj.get('enable_to', None)) - user_enable_to = user_obj.get('enable_to', None).astimezone(pytz.UTC) - log.debug(user_enable_to) - - if resp_data['enable']: pass + if user_rec_result.get('enable', None): + log.info('The user account is enabled') else: - log.info('The user account has been disabled') - if user_enable_from <= current_utc_datetime: - log.info('Enable from datetime is valid') - else: - log.info('Enable from datetime is in the future. Please wait.') - if user_enable_to >= current_utc_datetime: - log.info('Enable to datetime is valid') - else: - log.info('Enable to datetime is in the past. Your user account has been disabled.') + log.info('The user account is not enabled') + return mk_resp(data=False, status_message='This user account is not enabled') - update_data = {} - update_data['id'] = sql_select_result.get('id') - update_data['auth_key'] = None + if user_enable_from := user_rec_result.get('enable_from', None).astimezone(pytz.UTC): + if user_enable_from <= current_utc_datetime: + log.info('Enable from datetime is valid') + else: + log.info('Enable from datetime is in the future. Please wait.') + return mk_resp(data=False, status_message='This account is not yet enabled') - if sql_update_resp := sql_update(table_name='user', data=update_data): - log.info('The user record was updated with a NULL auth_key') - else: - log.info('The user record was not updated with a NULL auth_key') - log.debug(sql_update_resp) + if user_enable_to := user_rec_result.get('enable_to', None).astimezone(pytz.UTC): + if user_enable_to >= current_utc_datetime: + log.info('Enable to datetime is valid') + else: + log.info('Enable to datetime is in the past. Your user account has been disabled.') + return mk_resp(data=False, status_message='This account is not enabled because the expiratation date has passed') + user_obj = load_user_obj( + user_id=user_id, + inc_contact=inc_contact, + inc_organization=inc_organization, + inc_person=inc_person + ).dict(by_alias=by_alias, exclude_unset=exclude_unset) + data = user_obj return mk_resp(data=user_obj) else: - return mk_resp(data=None, status_code=404) + log.error('SQL result was unexpected. A dict result type was expected. This should not happen.') + return mk_resp(data=False, status_code=500) +# ### END ### API User Routers ### user_authenticate() ### @router.get('/list', response_model=Resp_Body_Base) @@ -239,9 +327,9 @@ async def lookup_user_obj( """ # This will return a list if selecting by account ID - user_obj_result = sql_select(data=data, sql=sql, as_list=as_list) - if isinstance(user_obj_result, dict): - user_id = user_obj_result.get('user_id', None) + user_rec_result = sql_select(data=data, sql=sql, as_list=as_list) + if isinstance(user_rec_result, dict): + user_id = user_rec_result.get('user_id', None) user_obj = load_user_obj( user_id=user_id, inc_contact=inc_contact, @@ -249,9 +337,9 @@ async def lookup_user_obj( inc_person=inc_person ).dict(by_alias=by_alias, exclude_unset=exclude_unset) data = user_obj - elif isinstance(user_obj_result, list): + elif isinstance(user_rec_result, list): user_obj_li = [] - for user_obj in user_obj_result: + for user_obj in user_rec_result: user_id = user_obj.get('user_id', None) user_obj_li.append( load_user_obj( @@ -263,7 +351,7 @@ async def lookup_user_obj( ) data = user_obj_li else: - log.debug(user_obj_result) + log.debug(user_rec_result) return mk_resp(data=None, status_code=404) # Not Found return mk_resp(data=data) @@ -327,23 +415,15 @@ async def lookup_username_obj( return mk_resp(data=data) -# Authenticate a username and password -@router.get('/authenticate', response_model=Resp_Body_Base) -async def user_authenticate( - username: str = Query(..., min_length=2, max_length=50), - password: str = Query(..., min_length=6, max_length=50), - x_account_id: str = Header(...), - ): - - return mk_resp(data=None, status_code=501) # Not Implemented - - @router.get('/{obj_id}', response_model=Resp_Body_Base) async def get_user_obj( obj_id: str = Query(..., min_length=1, max_length=22), x_account_id: str = Header(...), - by_alias: Optional[bool] = True, - exclude_unset: Optional[bool] = True, + inc_contact: bool = False, + inc_organization: bool = False, + inc_person: bool = False, + by_alias: bool = True, + exclude_unset: bool = True, ): log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals())